mirror of
https://github.com/Damillora/phoebe.git
synced 2025-03-10 14:07:22 +00:00
feat: add dedicated similarity search endpoint
This commit is contained in:
parent
fa81548e8a
commit
4c2fb159a9
@ -29,6 +29,10 @@ func InitializeBlobRoutes(g *gin.Engine) {
|
|||||||
{
|
{
|
||||||
protected.POST("/upload", uploadBlob)
|
protected.POST("/upload", uploadBlob)
|
||||||
}
|
}
|
||||||
|
unprotected := g.Group("/api/blob")
|
||||||
|
{
|
||||||
|
unprotected.POST("/search", searchBlob)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,14 +49,6 @@ func uploadBlob(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, models.ErrorResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
Message: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uuid.NewString()
|
id := uuid.NewString()
|
||||||
folder1 := id[0:2]
|
folder1 := id[0:2]
|
||||||
folder2 := id[2:4]
|
folder2 := id[2:4]
|
||||||
@ -205,3 +201,48 @@ func uploadBlob(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
return
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -24,5 +24,7 @@ type PostListItem struct {
|
|||||||
|
|
||||||
type PostSimilarityListItem struct {
|
type PostSimilarityListItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
ImagePath string `json:"image_path"`
|
||||||
|
ImageThumbnailPath string `json:"thumbnail_path"`
|
||||||
Distance int `json:"distance"`
|
Distance int `json:"distance"`
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,11 @@ type BlobSimilarResponse struct {
|
|||||||
PreviewUrl string `json:"previewUrl"`
|
PreviewUrl string `json:"previewUrl"`
|
||||||
Similar []PostSimilarityListItem `json:"similar"`
|
Similar []PostSimilarityListItem `json:"similar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SimilarResponse struct {
|
||||||
|
Similar []PostSimilarityListItem `json:"similar"`
|
||||||
|
}
|
||||||
|
|
||||||
type PostPaginationResponse struct {
|
type PostPaginationResponse struct {
|
||||||
CurrentPage int `json:"currentPage"`
|
CurrentPage int `json:"currentPage"`
|
||||||
TotalPage int `json:"totalPage"`
|
TotalPage int `json:"totalPage"`
|
||||||
|
@ -39,6 +39,8 @@ func SimilaritySearch(originalHashInt uint64) ([]models.PostSimilarityListItem,
|
|||||||
database.DB.Where("blob_id = ?", blob.ID).Find(&post)
|
database.DB.Where("blob_id = ?", blob.ID).Find(&post)
|
||||||
posts = append(posts, models.PostSimilarityListItem{
|
posts = append(posts, models.PostSimilarityListItem{
|
||||||
ID: post.ID,
|
ID: post.ID,
|
||||||
|
ImageThumbnailPath: "/data/" + blob.ThumbnailFilePath,
|
||||||
|
ImagePath: "/data/" + blob.FilePath,
|
||||||
Distance: distance,
|
Distance: distance,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,28 @@ export async function uploadBlob({ file, onProgress }) {
|
|||||||
return response.data;
|
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 }) {
|
export async function postCreate({ blob_id, source_url, tags }) {
|
||||||
const endpoint = url + "/api/post/create";
|
const endpoint = url + "/api/post/create";
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
<div class="navbar-start">
|
<div class="navbar-start">
|
||||||
<a class="navbar-item" href="/posts">Posts</a>
|
<a class="navbar-item" href="/posts">Posts</a>
|
||||||
<a class="navbar-item" href="/tags">Tags</a>
|
<a class="navbar-item" href="/tags">Tags</a>
|
||||||
|
<a class="navbar-item" href="/imagesearch">Image Search</a>
|
||||||
{#if loggedIn}
|
{#if loggedIn}
|
||||||
<a class="navbar-item" href="/upload">Upload</a>
|
<a class="navbar-item" href="/upload">Upload</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
144
pkg/web/src/routes/imagesearch/+page.svelte
Normal file
144
pkg/web/src/routes/imagesearch/+page.svelte
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user