diff --git a/api.go b/api.go
index e4bfea25a098b41333644ada5ec021cf3ce4e35d..1a034f49f18551f79d5e92c4cf57f0a6fd1bda09 100644
--- a/api.go
+++ b/api.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"random.chars.jp/git/image-board/store"
 	"strconv"
+	"strings"
 )
 
 type UserPayload struct {
@@ -271,6 +272,12 @@ func registerAPI() {
 		context.JSON(http.StatusOK, instance.UserImages(context.Param("flake")))
 	})
 
+	router.GET("/api/search/:tags", func(context *gin.Context) {
+		tagsPayload := context.Param("tags")
+		tags := strings.Split(tagsPayload, "!")
+		context.JSON(http.StatusOK, instance.ImageSearch(tags))
+	})
+
 	router.GET("/api/image/snowflake", func(context *gin.Context) {
 		info, ok := getUser(context)
 
@@ -378,11 +385,11 @@ func registerAPI() {
 		context.JSON(http.StatusOK, image)
 	})
 
-	router.GET("/api/image/:hash", func(context *gin.Context) {
-		context.JSON(http.StatusOK, instance.Image(context.Param("hash")))
+	router.GET("/api/image/:flake", func(context *gin.Context) {
+		context.JSON(http.StatusOK, instance.ImageSnowflake(context.Param("flake")))
 	})
 
-	router.PATCH("/api/image/:hash", func(context *gin.Context) {
+	router.PATCH("/api/image/:flake", func(context *gin.Context) {
 		user, ok := getUser(context)
 
 		// Require sign in
@@ -391,7 +398,7 @@ func registerAPI() {
 			return
 		}
 
-		info := instance.Image(context.Param("hash"))
+		info := instance.ImageSnowflake(context.Param("flake"))
 		if !user.Privileged && (info.User != user.Snowflake) {
 			context.JSON(http.StatusForbidden, denied)
 			return
@@ -408,7 +415,7 @@ func registerAPI() {
 		instance.ImageSource(info.Hash, payload.Source)
 	})
 
-	router.DELETE("/api/image/:hash", func(context *gin.Context) {
+	router.DELETE("/api/image/:flake", func(context *gin.Context) {
 		info, ok := getUser(context)
 
 		// Require sign in
@@ -417,17 +424,19 @@ func registerAPI() {
 			return
 		}
 
-		hash := context.Param("hash")
-		if !info.Privileged && (info.Snowflake != instance.Image(hash).User) {
+		flake := context.Param("flake")
+		image := instance.ImageSnowflake(flake)
+		if !info.Privileged && (info.Snowflake != image.User) {
 			context.JSON(http.StatusForbidden, denied)
 			return
 		}
-		instance.ImageDestroy(hash)
+		instance.ImageDestroy(image.Hash)
 	})
 
-	router.GET("/api/image/:hash/file", func(context *gin.Context) {
-		image, data := instance.ImageData(context.Param("hash"), false)
-		if image.Hash == "" {
+	router.GET("/api/image/:flake/file", func(context *gin.Context) {
+		flake := context.Param("flake")
+		image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), false)
+		if image.Snowflake == flake {
 			context.JSON(http.StatusNotFound, gin.H{
 				"error": "not found",
 			})
@@ -436,9 +445,10 @@ func registerAPI() {
 		context.Data(http.StatusOK, "image/"+image.Type, data)
 	})
 
-	router.GET("/api/image/:hash/preview", func(context *gin.Context) {
-		image, data := instance.ImageData(context.Param("hash"), true)
-		if image.Hash == "" {
+	router.GET("/api/image/:flake/preview", func(context *gin.Context) {
+		flake := context.Param("flake")
+		image, data := instance.ImageData(instance.ImageSnowflakeHash(flake), true)
+		if image.Snowflake == flake {
 			context.JSON(http.StatusNotFound, gin.H{
 				"error": "not found",
 			})
@@ -447,11 +457,11 @@ func registerAPI() {
 		context.Data(http.StatusOK, "image/jpeg", data)
 	})
 
-	router.GET("/api/image/:hash/tag", func(context *gin.Context) {
-		context.JSON(http.StatusOK, instance.ImageTags(context.Param("hash")))
+	router.GET("/api/image/:flake/tag", func(context *gin.Context) {
+		context.JSON(http.StatusOK, instance.ImageTags(context.Param("flake")))
 	})
 
-	router.PUT("/api/image/:hash/tag/:tag", func(context *gin.Context) {
+	router.PUT("/api/image/:flake/tag/:tag", func(context *gin.Context) {
 		info, ok := getUser(context)
 
 		// Require sign in
@@ -460,15 +470,15 @@ func registerAPI() {
 			return
 		}
 
-		hash := context.Param("hash")
-		if !info.Privileged && (info.Snowflake != instance.Image(hash).User) {
+		flake := context.Param("flake")
+		if !info.Privileged && (info.Snowflake != instance.ImageSnowflake(flake).User) {
 			context.JSON(http.StatusForbidden, denied)
 			return
 		}
-		instance.ImageTagAdd(hash, context.Param("tag"))
+		instance.ImageTagAdd(flake, context.Param("tag"))
 	})
 
-	router.DELETE("/api/image/:hash/tag/:tag", func(context *gin.Context) {
+	router.DELETE("/api/image/:flake/tag/:tag", func(context *gin.Context) {
 		info, ok := getUser(context)
 
 		// Require sign in
@@ -477,12 +487,12 @@ func registerAPI() {
 			return
 		}
 
-		hash := context.Param("hash")
-		if !info.Privileged && (info.Snowflake != instance.Image(hash).User) {
+		flake := context.Param("flake")
+		if !info.Privileged && (info.Snowflake != instance.ImageSnowflake(flake).User) {
 			context.JSON(http.StatusForbidden, denied)
 			return
 		}
-		instance.ImageTagRemove(hash, context.Param("tag"))
+		instance.ImageTagRemove(flake, context.Param("tag"))
 	})
 
 	router.GET("/api/tag", func(context *gin.Context) {
diff --git a/store/image.go b/store/image.go
index 5e57acfd27d4b68c823e133e4b5f67dafb107358..323fa45865aaeb884c115c25b1ede8821e06cdaa 100644
--- a/store/image.go
+++ b/store/image.go
@@ -129,6 +129,81 @@ func (s *Store) imageTags(flake string) []string {
 	return tags
 }
 
+// ImageHasTag figures out if an image has a tag.
+func (s *Store) ImageHasTag(flake string, tag string) bool {
+	return s.file(s.ImageTagsPath(flake) + "/" + tag)
+}
+
+//ImageSearch searches for images with specific tags.
+func (s *Store) ImageSearch(tags []string) []string {
+	if len(tags) < 1 || tags == nil {
+		return nil
+	}
+
+	// Check if every tag matches name regex and exists
+	for _, tag := range tags {
+		if !nameRegex.MatchString(tag) || !s.file(s.TagPath(tag)) {
+			return nil
+		}
+	}
+
+	// Return if there's only one tag to search for
+	if len(tags) == 1 {
+		return s.Tag(tags[0])
+	}
+
+	// Find entry with the least pages
+	entry := struct {
+		min   int
+		index int
+	}{
+		min:   s.PageTotal("tag_" + tags[0]),
+		index: 0,
+	}
+	for i := 1; i < len(tags); i++ {
+		if entry.min <= 1 {
+			break
+		}
+
+		pages := s.PageTotal("tag_" + tags[i])
+		if pages < entry.min {
+			entry.min = pages
+			entry.index = i
+		}
+	}
+
+	// Get initial tag
+	initial := s.Tag(tags[entry.index])
+
+	// Result slice
+	var result []string
+
+	// Walk flakes from initial tag
+	for _, flake := range initial {
+		match := true
+		// Walk all remaining tags
+		for i, tag := range tags {
+			// Skip the entrypoint entry
+			if i == entry.index {
+				continue
+			}
+
+			// Check if match
+			if !s.ImageHasTag(flake, tag) {
+				match = false
+				break
+			}
+		}
+
+		// Append flake if all tags matched
+		if match {
+			result = append(result, flake)
+		}
+	}
+
+	return result
+}
+
 // ImageAdd adds an image to the store.
 func (s *Store) ImageAdd(data []byte, flake string) Image {
 	if !s.flake(flake) || !s.dir(s.UserPath(flake)) {