Andrew Smith commited on
Commit
48e00e6
·
1 Parent(s): ad7257f

Initial commit

Browse files
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ __pycache__
2
+ .vscode
3
+ supabase/
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ WORKDIR /code
4
+
5
+ COPY . /code
6
+
7
+ # Get secret DB_URL and clone it as repo at buildtime
8
+ RUN --mount=type=secret,id=DB_URL,mode=0444,required=true \
9
+ git clone $(cat /run/secrets/DB_URL)
10
+
11
+ RUN pip install --no-cache-dir poetry
12
+
13
+ RUN poetry config virtualenvs.create false \
14
+ && poetry install --no-dev
15
+
16
+ RUN poetry run search
17
+
18
+ CMD ["poetry", "run", "start"]
image_search/main.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Union
3
+ from PIL import Image
4
+ from fastapi import FastAPI
5
+ from fastapi.staticfiles import StaticFiles
6
+ from fastapi.responses import FileResponse
7
+ from sentence_transformers import SentenceTransformer
8
+ import uvicorn
9
+ import vecs
10
+
11
+ DB_CONNECTION = os.environ.get(
12
+ 'DB_URL', "postgresql://postgres:postgres@localhost:54322/postgres")
13
+
14
+ app = FastAPI()
15
+
16
+
17
+ @app.get("/seed")
18
+ def seed():
19
+ # create vector store client
20
+ vx = vecs.create_client(DB_CONNECTION)
21
+
22
+ iv = vx.get_collection(name="image_vectors")
23
+
24
+ if iv:
25
+ return {"message": "Collection already exists."}
26
+
27
+ # create a collection of vectors with 512 dimensions
28
+ images = vx.create_collection(name="image_vectors", dimension=512)
29
+
30
+ # Load CLIP model
31
+ model = SentenceTransformer('clip-ViT-B-32')
32
+
33
+ # Encode an image:
34
+ img_emb1 = model.encode(Image.open('./images/one.jpg'))
35
+ img_emb2 = model.encode(Image.open('./images/two.jpg'))
36
+ img_emb3 = model.encode(Image.open('./images/three.jpg'))
37
+ img_emb4 = model.encode(Image.open('./images/four.jpg'))
38
+
39
+ images.upsert(
40
+ vectors=[
41
+ (
42
+ "one.jpg",
43
+ img_emb1,
44
+ {"type": "jpg"}
45
+ ), (
46
+ "two.jpg",
47
+ img_emb2,
48
+ {"type": "jpg"}
49
+ ), (
50
+ "three.jpg",
51
+ img_emb3,
52
+ {"type": "jpg"}
53
+ ), (
54
+ "four.jpg",
55
+ img_emb4,
56
+ {"type": "jpg"}
57
+ )
58
+ ]
59
+ )
60
+ print("Inserted images")
61
+
62
+ # index the collection fro fast search performance
63
+ images.create_index()
64
+ return {"message": "Collection created and indexed."}
65
+
66
+
67
+ @app.get("/search")
68
+ def search(query: Union[str, None] = None):
69
+ # create vector store client
70
+ vx = vecs.create_client(DB_CONNECTION)
71
+ images = vx.get_collection(name="image_vectors")
72
+
73
+ # Load CLIP model
74
+ model = SentenceTransformer('clip-ViT-B-32')
75
+ # Encode text query
76
+ query_string = query
77
+ text_emb = model.encode(query_string)
78
+
79
+ # query the collection filtering metadata for "type" = "jpg"
80
+ results = images.query(
81
+ query_vector=text_emb,
82
+ limit=1,
83
+ filters={"type": {"$eq": "jpg"}},
84
+ )
85
+ result = results[0]
86
+ return {"result": result, "query": query}
87
+
88
+
89
+ app.mount("/images", StaticFiles(directory="images"), name="images")
90
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
91
+
92
+
93
+ @app.get("/")
94
+ def index() -> FileResponse:
95
+ return FileResponse(path="static/index.html", media_type="text/html")
96
+
97
+
98
+ def start():
99
+ """Launched with `poetry run start` at root level"""
100
+ uvicorn.run("image_search.main:app",
101
+ host="0.0.0.0", port=7860, reload=True)
images/four.jpg ADDED
images/one.jpg ADDED
images/three.jpg ADDED
images/two.jpg ADDED
poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
pyproject.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "image-search"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = ["Andrew Smith <[email protected]>"]
6
+ readme = "README.md"
7
+ packages = [{include = "image_search"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ vecs = "^0.2.6"
12
+ sentence-transformers = "^2.2.2"
13
+ fastapi = "^0.99.1"
14
+ uvicorn = {extras = ["standard"], version = "^0.22.0"}
15
+
16
+ [tool.poetry.scripts]
17
+ start = "image_search.main:start"
18
+ seed = "image_search.main:seed"
19
+ search = "image_search.main:search"
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"
static/index.html ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Image Search</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ <script type="module" src="script.js"></script>
9
+ </head>
10
+ <body>
11
+ <main>
12
+ <h1>Image Search</h1>
13
+ <form class="search-form">
14
+ <label>Name </label>
15
+ <input id="query" type="text" name="query">
16
+ <button class="submit-button">Submit</button>
17
+ </form>
18
+ <div class="result"></div>
19
+ </main>
20
+ </body>
21
+ </html>
static/script.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const searchForm = document.querySelector(".search-form");
2
+ const result = document.querySelector(".result");
3
+ const submitButton = document.querySelector(".submit-button");
4
+
5
+ const clearDiv = () => {
6
+ result.innerHTML = "";
7
+ };
8
+
9
+ const buttonLoading = () => {
10
+ submitButton.innerHTML = "loading...";
11
+ submitButton.setAttribute("disabled", true);
12
+ };
13
+
14
+ const buttonDefault = () => {
15
+ submitButton.innerHTML = "Submit";
16
+ submitButton.removeAttribute("disabled");
17
+ };
18
+
19
+ const renderImg = (imageName) => {
20
+ clearDiv();
21
+ const img = document.createElement("img");
22
+ img.src = `/images/${imageName}`;
23
+ result.appendChild(img);
24
+ };
25
+
26
+ const getResult = async (searchQuery) => {
27
+ const response = await fetch(`search?query=${searchQuery}`);
28
+ const json = await response.json();
29
+
30
+ return json.result;
31
+ };
32
+
33
+ searchForm.addEventListener("submit", async (event) => {
34
+ event.preventDefault();
35
+
36
+ const query = document.querySelector("#query");
37
+ buttonLoading();
38
+ try {
39
+ const res = await getResult(query.value);
40
+ renderImg(res);
41
+ } catch (err) {
42
+ console.error(err);
43
+ clearDiv();
44
+ } finally {
45
+ buttonDefault();
46
+ }
47
+ });
static/style.css ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ main {
2
+ display: flex;
3
+ flex-direction: column;
4
+ justify-content: center;
5
+ align-items: center;
6
+ }
7
+ .search-form {
8
+ padding: 20px 0;
9
+ }
tests/__init__.py ADDED
File without changes