feat: add dedicated similarity search endpoint

This commit is contained in:
Damillora 2025-02-24 11:41:31 +00:00
parent fa81548e8a
commit 4c2fb159a9
7 changed files with 227 additions and 10 deletions

View File

@ -29,6 +29,10 @@ func InitializeBlobRoutes(g *gin.Engine) {
{
protected.POST("/upload", uploadBlob)
}
unprotected := g.Group("/api/blob")
{
unprotected.POST("/search", searchBlob)
}
}
@ -45,14 +49,6 @@ func uploadBlob(c *gin.Context) {
return
}
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
id := uuid.NewString()
folder1 := id[0:2]
folder2 := id[2:4]
@ -205,3 +201,48 @@ func uploadBlob(c *gin.Context) {
}
return
}
func searchBlob(c *gin.Context) {
// Source
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
fileObj, _ := file.Open()
originalImage, _, err := image.Decode(fileObj)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
}
hash, err := goimagehash.PerceptionHash(originalImage)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Code: http.StatusBadRequest,
Message: err.Error(),
})
return
}
hashInt := hash.GetHash()
similarPosts, err := services.SimilaritySearch(hashInt)
if err != nil {
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Code: http.StatusInternalServerError,
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK,
models.SimilarResponse{
Similar: similarPosts,
})
}

View File

@ -24,5 +24,7 @@ type PostListItem struct {
type PostSimilarityListItem struct {
ID string `json:"id"`
ImagePath string `json:"image_path"`
ImageThumbnailPath string `json:"thumbnail_path"`
Distance int `json:"distance"`
}

View File

@ -28,6 +28,11 @@ type BlobSimilarResponse struct {
PreviewUrl string `json:"previewUrl"`
Similar []PostSimilarityListItem `json:"similar"`
}
type SimilarResponse struct {
Similar []PostSimilarityListItem `json:"similar"`
}
type PostPaginationResponse struct {
CurrentPage int `json:"currentPage"`
TotalPage int `json:"totalPage"`

View File

@ -38,8 +38,10 @@ func SimilaritySearch(originalHashInt uint64) ([]models.PostSimilarityListItem,
var post database.Post
database.DB.Where("blob_id = ?", blob.ID).Find(&post)
posts = append(posts, models.PostSimilarityListItem{
ID: post.ID,
Distance: distance,
ID: post.ID,
ImageThumbnailPath: "/data/" + blob.ThumbnailFilePath,
ImagePath: "/data/" + blob.FilePath,
Distance: distance,
})
}
}

View File

@ -123,6 +123,28 @@ export async function uploadBlob({ file, onProgress }) {
return response.data;
}
export async function searchBlob({ file, onProgress }) {
var formData = new FormData();
formData.append("file", file);
const endpoint = url + "/api/blob/search";
const response = await axios({
url: endpoint,
method: "POST",
headers: {
'Authorization': 'Bearer ' + current_token,
'Content-Type': 'multipart/form-data',
},
withCredentials: true,
data: formData,
onUploadProgress: e => {
if (onProgress) {
onProgress(e)
}
}
})
return response.data;
}
export async function postCreate({ blob_id, source_url, tags }) {
const endpoint = url + "/api/post/create";
const response = await axios({

View File

@ -41,6 +41,7 @@
<div class="navbar-start">
<a class="navbar-item" href="/posts">Posts</a>
<a class="navbar-item" href="/tags">Tags</a>
<a class="navbar-item" href="/imagesearch">Image Search</a>
{#if loggedIn}
<a class="navbar-item" href="/upload">Upload</a>
{/if}

View File

@ -0,0 +1,144 @@
<script>
import { getTagAutocomplete, searchBlob } from "$lib/api";
import PostGallery from "$lib/components/ui/PostGallery.svelte";
import ShiorikoImage from "$lib/components/ui/ShiorikoImage.svelte";
import { paginate } from "$lib/simple-pagination";
let currentProgress = $state(0);
let file = $state();
let fileName = $state("");
let similar = $state([]);
let similarCount = $state(0);
let loading = $state(false);
let loaded = $state(false);
let form = $state({
blob_id: "",
source_url: "",
tags: [],
});
const onProgress = (e) => {
var percentCompleted = Math.round((e.loaded * 100) / e.total);
currentProgress = percentCompleted;
};
const onFileChange = async (e) => {
file = e.target.files[0];
fileName = file.name;
};
const onSubmit = async (e) => {
e.preventDefault();
loading = true;
loaded = false;
similar = [];
if (file) {
var response = await searchBlob({ file, onProgress });
similar = response.similar;
similarCount = similar.length;
}
loading = false;
loaded = true;
};
</script>
<section class="section">
<div class="container">
<div class="columns">
<div class="column is-one-third">
<div class="panel is-primary">
<form onsubmit={onSubmit}>
<p class="panel-heading">Image Search</p>
<div class="panel-block column">
<div class="row">
<label for="file" class="label">Image:</label>
</div>
<div class="row">
<div class="field">
<div class="control">
<div class="file">
<label class="file-label">
<input
id="file"
class="file-input"
type="file"
name="resume"
onchange={onFileChange}
/>
<span class="file-cta">
<span class="file-icon"
></span>
<span class="file-label">
Choose a file…
</span>
</span>
</label>
</div>
</div>
{#if fileName}
<p class="help">{fileName}</p>
{/if}
</div>
</div>
</div>
{#if currentProgress > 0 && currentProgress < 100}
<div class="panel-block">
<progress
class="progress is-primary is-small"
value={currentProgress}
max="100"
>
{currentProgress}%
</progress>
</div>
{/if}
<div class="panel-block column">
<button
type="submit"
class="button is-primary is-fullwidth is-outlined"
>Search</button
>
</div>
</form>
</div>
</div>
<div class="column is-two-thirds">
{#if !loading}
{#if loaded}
{#if similarCount > 0}
<div class="block">
<div class="notification is-success">
Found {similarCount} similar posts.
</div>
</div>
{:else}
<div class="block">
<div class="notification is-warning">
Found no similar posts.
</div>
</div>
{/if}
{:else}
<div class="notification is-primary">
Similar posts will appear here.
</div>
{/if}
{/if}
<div class="columns is-multiline">
{#if !loading}
{#if similarCount > 0}
<div class="column is-full">
<PostGallery posts={similar} />
</div>
{/if}
{:else}
<div class="column">
<div class="skeleton-block"></div>
</div>
{/if}
</div>
</div>
</div>
</div>
</section>