Skip to content

Commit

Permalink
Add search endpoint to API
Browse files Browse the repository at this point in the history
Add cache to search API call
Add search API URL env var
Add sitemap (more to come)
Add search endpoints and search bar to home
  • Loading branch information
mskarlin committed Feb 18, 2021
1 parent 98b508c commit a6e3b7c
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ notebooks/*.csv
*.xml
*.pkl
*.npy
image_model_api/models/*.txt
cloud_functions/resize_images/node_modules/*
build_deploy/build/*
*.DS_Store
Expand Down
24 changes: 23 additions & 1 deletion art_snob_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,28 @@ def random(session_id=None, cursor='0_25', curated=True):
# recommendations.update({'session_id': session_id})
return {'art': work_list, 'cursor': f"{start+n_items}_{n_items}"}

@app.get('/search/{query}')
def search(query: str, start_cursor: str = None, n_records: int = 26, session_id=None):
if not session_id:
session_id = str(uuid.uuid4())

seed = rand.randint(0,10000)
start = 0

if start_cursor:
seed, start = start_cursor.split('_')
seed = int(seed)
start = int(start)

works = data.search_api(query, start=start, n_records=n_records)

work_list = list_and_add_image_prefix({'art': works}, hydration_dict=None)

log_exposure(work_list, session_id, how=f"exposure:search:{query}")
data.write_action(Action(session=session_id, action='search', item=query))

return {'art': work_list, 'cursor': f'{seed}_{start+n_records}'}

@app.get('/tags/{tag}')
def tags(tag: str, start_cursor: str = None, n_records: int = 10, session_id=None, return_clusters=False):
if not session_id:
Expand Down Expand Up @@ -235,7 +257,7 @@ def recommended(session_id=None, likes:str='', dislikes:str='', start_cursor=Non
seed+=rand.randint(0,1000)

works = []
cdata, _ = data.clusters(likes, seed=seed, cursor=start_cursor, n_records=int(n_return), session_id=session_id)
cdata, _ = data.clusters(likes, seed=seed, cursor=start_cursor, n_records=int(n_return), session_id=session_id, include_search=True)
work_list = list_and_add_image_prefix({'art': cdata})

log_exposure(work_list, session_id, how=f"exposure:recommended:{likes}|{dislikes}")
Expand Down
45 changes: 44 additions & 1 deletion art_snob_api/src/datastore_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import os
from src.ordered_set import OrderedSet
from collections import Counter
import cachetools
from typing import List
import requests
from threading import RLock
from cachetools.keys import hashkey

import itertools
import sys

Expand All @@ -26,6 +31,11 @@ def roundrobin(*iterables):
num_active -= 1
nexts = itertools.cycle(itertools.islice(nexts, num_active))

def search_key(*args, **kwargs):
"""custom hashkey builder for caching"""
key = hashkey(*args, **kwargs)
return key


class FriendlyDataStore():
ACTION_KIND = 'prod-action-stream'
Expand All @@ -40,13 +50,16 @@ class FriendlyDataStore():
CLUSTER_INDEX = '12122020-cluster-index'
STATE_KIND = '12202020-state'
STATE_LOGIN = '12202020-login'
SEARCH_API_URL = os.environ.get('SEARCH_API_URL', 'http://localhost:8001')
# RAND_MIN = 4503653962481664 # used for scraped-image-data indices
# RAND_MAX = 6755350696951808
RAND_MIN = 1
RAND_MAX = 10001

def __init__(self, dsi=None):
self.dsi = dsi if dsi else DataStoreInterface(os.environ.get('GOOGLE_CLOUD_PROJECT'))
self.cache = cachetools.TTLCache(5000, 600) # 5000 items at 600 seconds
self.lock = lock = RLock()

def write_action(self, action):
write_dict = {}
Expand Down Expand Up @@ -238,7 +251,8 @@ def rerank_from_like_and_approvals(self, pos_neighbors, neg_neighbors, exposed_c
# remove what was below the threshold prior (that's all seen...)
return [t[0] for t in sorted(indexed_id_list_to_rank, key=lambda x: x[1], reverse=True)]

def clusters(self, clusters: List, seed=814, cursor:str='', n_records:int=26, session_id=None, from_center=False, include_description=False):
def clusters(self, clusters: List, seed=814, cursor:str='', n_records:int=26, session_id=None,
from_center=False, include_description=False, include_search=False):
"""Get recommendation results, sorted contextually. Can also get descriptions for clusters (only when ranked from center)
TODO: much of the optional logic for include_description and from_center may be unecessary..
"""
Expand All @@ -253,6 +267,23 @@ def clusters(self, clusters: List, seed=814, cursor:str='', n_records:int=26, se

cluster_keys = self.dsi.read(ids=clusters, kind=self.CLUSTER_REVERSE_INDEX, sorted_list=True)

# add in the search results as seeds too, randomly including up to 3
if include_search:
results, cursor = self.dsi.query_nocache(kind=self.ACTION_KIND,
query_filters=[('session', '=', session_id),
('action', '=', 'search')
],
n_records=10,
tolist=True
)

choices = {r['item'] for r in results}
choices = choices if len(choices) < 3 else random.sample(choices, 3)

for query in choices:
cluster_keys.append({'idx': self.search_api(query, start=0, n_records=200, neighbor_ids=False),
'description': 'search results'})

idx_to_request = []
description = None

Expand All @@ -263,6 +294,7 @@ def clusters(self, clusters: List, seed=814, cursor:str='', n_records:int=26, se
random.Random(rseed).shuffle(tmp_cluster_ids)

if session_id:
#TODO: don't call this every loop iteration
pos_neighbors, neg_neighbors, exposed_counter = self.member_neighbor_sets(session_id)
tmp_cluster_ids = self.rerank_from_like_and_approvals(pos_neighbors, neg_neighbors, exposed_counter, tmp_cluster_ids)[start:]
else:
Expand All @@ -281,7 +313,18 @@ def clusters(self, clusters: List, seed=814, cursor:str='', n_records:int=26, se
return self.dsi.read(ids=idx_to_request[:n_records], kind=self.INFO_KIND), description
else:
return self.dsi.read(ids=idx_to_request[start:(start+n_records)], kind=self.INFO_KIND), description

@cachetools.cachedmethod(lambda self: self.cache, lock=lambda self: self.lock)
def search_api(self, query, start=0, n_records=25, neighbor_ids=False):

r = requests.get(f'{self.SEARCH_API_URL}/semantic_neighbors/?query={query}&n_start={start}&n_records={n_records}')
response_ids = r.json()['neighbors']

if neighbor_ids:
return response_ids[:n_records]
else:
return self.dsi.read(ids=response_ids[:n_records], kind=self.INFO_KIND)

def search(self, query, get_cursor=False, start_cursor=None, n_records=25):
"""Get all search results based on tags"""
queries = query.lower().split(' ')
Expand Down
3 changes: 3 additions & 0 deletions art_snob_frontend/public/sitemap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://artsnob.io/
https://artsnob.io/taste
https://artsnob.io/about
29 changes: 28 additions & 1 deletion art_snob_frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {TasteFinder} from "./tasteFinder"
import {About} from "./about.js"
import {ArtDetail, ArtCarousel, SingleCarousel} from "./detailView"
import {RoomConfigurations} from "./roomConfiguration.js"
import {ArtBrowse} from "./artBrowse"
import {ArtBrowse, Search} from "./artBrowse"
import {Privacy} from "./privacy.js"
import {Terms} from "./terms.js"
import {History} from "./history.js"
Expand All @@ -32,6 +32,8 @@ import HelpIcon from '@material-ui/icons/Help';
import NewReleasesIcon from '@material-ui/icons/NewReleases';
import AssignmentIcon from '@material-ui/icons/Assignment';
import HomeIcon from '@material-ui/icons/Home';
import TextField from '@material-ui/core/TextField';

import { Helmet } from "react-helmet";


Expand Down Expand Up @@ -66,6 +68,7 @@ function App() {
<Rooms path="/walls"/>
<SharedRoom path="/shared/:sessionId/:wallId"/>
<ArtBrowse path="/browse/:id"/>
<Search path="/search/:query"/>
<ArtDetail path="/detail/:id"/>
<PurchaseList path="/purchase/:id"/>
<History path="/history"/>
Expand Down Expand Up @@ -126,6 +129,19 @@ function AppParent({children}) {
function LandingPage() {
const globalState = useContext(store);
const { state, dispatch } = globalState;
const [homeSearch, setHomeSearch] = useState('')

const handleHomeSearch = (event) => {
if (event){
setHomeSearch(event.target.value)
}
}

const homeSearchKeyPress = (e) => {
if((e.keyCode == 13) & (homeSearch != '')){
navigate('/search/'+homeSearch)
}
}

const baseMatch = useMatch('/')
if (baseMatch) {
Expand All @@ -147,6 +163,17 @@ function LandingPage() {
style={{"pointerEvents": "all"}}>
Start the taste finder
</Button>
<Typography variant="subtitle1" align="center">or</Typography>
<TextField style={{'width': '199px'}}
size={'small'}
margin={'dense'}
onChange={handleHomeSearch}
value={homeSearch}
onKeyDown={homeSearchKeyPress}
variant="outlined"
label="Search for art..."
placeholder="Try abstract or beach art"
/>
</div>
<ArtCarousel endpoints={['/random/']} showTitle={false} imgSize={'small'}/>
<LandingCopy/>
Expand Down

0 comments on commit a6e3b7c

Please sign in to comment.