diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 3a3a02c8a..7e4a6aa4d 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -144,7 +144,7 @@ If a new search engine backend class is to be implemented, it must closely follo utils.search.SearchEngineBase docstrings. There is a Django management command that can be used in order to test the implementation of a search backend. You can run it like: - docker-compose run --rm web python manage.py test_search_engine_backend -fsw --backend utils.search.backends.solr9pysolr.Solr9PySolrSearchEngine + docker compose run --rm web python manage.py test_search_engine_backend -fsw --backend utils.search.backends.solr9pysolr.Solr9PySolrSearchEngine Please read carefully the documentation of the management command to better understand how it works and how is it doing the testing. @@ -217,7 +217,7 @@ https://github.com/mtg/freesound-audio-analyzers. The docker compose of the main services for the external analyzers which depend on docker images having been previously built from the `freesound-audio-analyzers` repository. To build these images you simply need to checkout the code repository and run `make`. Once the images are built, Freesound can be run including the external analyzer services by of the docker compose -file by running `docker-compose --profile analyzers up` +file by running `docker compose --profile analyzers up` The new analysis pipeline uses a job queue based on Celery/RabbitMQ. RabbitMQ console can be accessed at port `5673` (e.g. `http://localhost:5673/rabbitmq-admin`) and using `guest` as both username and password. Also, accessing @@ -231,7 +231,7 @@ for Freesound async tasks other than analysis). - Make sure that there are no outstanding deprecation warnings for the version of django that we are upgrading to. - docker-compose run --rm web python -Wd manage.py test + docker compose run --rm web python -Wd manage.py test Check for warnings of the form `RemovedInDjango110Warning` (TODO: Make tests fail if a warning occurs) diff --git a/README.md b/README.md index d637b50f3..9e2ff7a93 100644 --- a/README.md +++ b/README.md @@ -65,35 +65,35 @@ Below are instructions for setting up a local Freesound installation for develop 8. Build all Docker containers. The first time you run this command can take a while as a number of Docker images need to be downloaded and things need to be installed and compiled. - docker-compose build + docker compose build 9. Download the [Freesound development database dump](https://drive.google.com/file/d/11z9s8GyYkVlmWdEsLSwUuz0AjZ8cEvGy/view?usp=share_link) (~6MB), uncompress it and place the resulting `freesound-small-dev-dump-2023-09.sql` in the `freesound-data/db_dev_dump/` directory. Then run the database container and load the data into it using the commands below. You should get permission to download this file from Freesound admins. - docker-compose up -d db - docker-compose run --rm db psql -h db -U freesound -d freesound -f freesound-data/db_dev_dump/freesound-small-dev-dump-2023-09.sql + docker compose up -d db + docker compose run --rm db psql -h db -U freesound -d freesound -f freesound-data/db_dev_dump/freesound-small-dev-dump-2023-09.sql # or if the above command does not work, try this one - docker-compose run --rm --no-TTY db psql -h db -U freesound -d freesound < freesound-data/db_dev_dump/freesound-small-dev-dump-2023-09.sql + docker compose run --rm --no-TTY db psql -h db -U freesound -d freesound < freesound-data/db_dev_dump/freesound-small-dev-dump-2023-09.sql 10. Update database by running Django migrations - docker-compose run --rm web python manage.py migrate + docker compose run --rm web python manage.py migrate 11. Create a superuser account to be able to log in to the local Freesound website and to the admin site - docker-compose run --rm web python manage.py createsuperuser + docker compose run --rm web python manage.py createsuperuser 12. Install static build dependencies - docker-compose run --rm web npm install --force + docker compose run --rm web npm install --force 13. Build static files. Note that this step will need to be re-run every time there are changes in Freesound's static code (JS, CSS and static media files). - docker-compose run --rm web npm run build - docker-compose run --rm web python manage.py collectstatic --noinput + docker compose run --rm web npm run build + docker compose run --rm web python manage.py collectstatic --noinput 14. Run services 🎉 - docker-compose up + docker compose up When running this command, the most important services that make Freesound work will be run locally. This includes the web application and database, but also the search engine, cache manager, queue manager and asynchronous workers, including audio processing. @@ -102,24 +102,24 @@ Below are instructions for setting up a local Freesound installation for develop 15. Build the search index, so you can search for sounds and forum posts # Open a new terminal window so the services started in the previous step keep running - docker-compose run --rm web python manage.py reindex_search_engine_sounds - docker-compose run --rm web python manage.py reindex_search_engine_forum + docker compose run --rm web python manage.py reindex_search_engine_sounds + docker compose run --rm web python manage.py reindex_search_engine_forum After following the steps, you'll have a functional Freesound installation up and running, with the most relevant services properly configured. You can run Django's shell plus command like this: - docker-compose run --rm web python manage.py shell_plus + docker compose run --rm web python manage.py shell_plus Because the `web` container mounts a named volume for the home folder of the user running the shell plus process, command history should be kept between container runs :) -16. (extra step) The steps above will get Freesound running, but to save resources in your local machine some non-essential services will not be started by default. If you look at the `docker-compose.yml` file, you'll see that some services are marked with the profile `analyzers` or `all`. These services include sound similarity, search results clustering and the audio analyzers. To run these services you need to explicitly tell `docker-compose` using the `--profile` (note that some services need additional configuration steps (see *Freesound analysis pipeline* section in `DEVELOPERS.md`): +16. (extra step) The steps above will get Freesound running, but to save resources in your local machine some non-essential services will not be started by default. If you look at the `docker compose.yml` file, you'll see that some services are marked with the profile `analyzers` or `all`. These services include sound similarity, search results clustering and the audio analyzers. To run these services you need to explicitly tell `docker compose` using the `--profile` (note that some services need additional configuration steps (see *Freesound analysis pipeline* section in `DEVELOPERS.md`): - docker-compose --profile analyzers up # To run all basic services + sound analyzers - docker-compose --profile all up # To run all services + docker compose --profile analyzers up # To run all basic services + sound analyzers + docker compose --profile all up # To run all services ### Running tests You can run tests using the Django test runner in the `web` container like that: - docker-compose run --rm web python manage.py test --settings=freesound.test_settings + docker compose run --rm web python manage.py test --settings=freesound.test_settings diff --git a/_docs/api/source/resources.rst b/_docs/api/source/resources.rst index bb96e3841..6c5c1259c 100644 --- a/_docs/api/source/resources.rst +++ b/_docs/api/source/resources.rst @@ -80,7 +80,7 @@ Filter name Type Description ``avg_rating`` numerical Average rating for the sound in the range [0, 5]. ``num_ratings`` integer Number of times the sound has been rated. ``comment`` string Textual content of the comments of a sound (tokenized). The filter is satisfied if sound contains the filter value in at least one of its comments. -``comments`` integer Number of times the sound has been commented. +``num_comments`` integer Number of times the sound has been commented. ====================== ============= ==================================================== diff --git a/freesound.code-workspace b/freesound.code-workspace index b159aadd8..4dc94ba62 100644 --- a/freesound.code-workspace +++ b/freesound.code-workspace @@ -34,28 +34,23 @@ "tasks": { "version": "2.0.0", "tasks": [ - { - "label": "Run web and search", - "type": "shell", - "command": "docker-compose up web search", - "problemMatcher": [] - }, + { "label": "Docker compose build", "type": "shell", - "command": "docker-compose build", + "command": "docker compose build", "problemMatcher": [] }, { "label": "Build static", "type": "shell", - "command": "docker-compose run --rm web npm run build && docker-compose run --rm web python manage.py collectstatic --clear --noinput", + "command": "docker compose run --rm web npm run build && docker compose run --rm web python manage.py collectstatic --clear --noinput", "problemMatcher": [] }, { "label": "Install static", "type": "shell", - "command": "docker-compose run --rm web npm install --force", + "command": "docker compose run --rm web npm install --force", "problemMatcher": [] }, { @@ -67,37 +62,55 @@ { "label": "Create caches", "type": "shell", - "command": "docker-compose run --rm web python manage.py create_front_page_caches && docker-compose run --rm web python manage.py create_random_sounds && docker-compose run --rm web python manage.py generate_geotags_bytearray", + "command": "docker compose run --rm web python manage.py create_front_page_caches && docker compose run --rm web python manage.py create_random_sounds && docker compose run --rm web python manage.py generate_geotags_bytearray", "problemMatcher": [] }, { "label": "Run tests", "type": "shell", - "command": "docker-compose run --rm web python manage.py test --settings=freesound.test_settings", + "command": "docker compose run --rm web python manage.py test --settings=freesound.test_settings", "problemMatcher": [] }, { "label": "Run tests verbose with warnings", "type": "shell", - "command": "docker-compose run --rm web python -Wa manage.py test -v3 --settings=freesound.test_settings", + "command": "docker compose run --rm web python -Wa manage.py test -v3 --settings=freesound.test_settings", "problemMatcher": [] }, { "label": "Migrate", "type": "shell", - "command": "docker-compose run --rm web python manage.py migrate", + "command": "docker compose run --rm web python manage.py migrate", "problemMatcher": [] }, { "label": "Make migrations", "type": "shell", - "command": "docker-compose run --rm web python manage.py makemigrations", + "command": "docker compose run --rm web python manage.py makemigrations", "problemMatcher": [] }, { "label": "Shell plus", "type": "shell", - "command": "docker-compose run --rm web python manage.py shell_plus", + "command": "docker compose run --rm web python manage.py shell_plus", + "problemMatcher": [] + }, + { + "label": "Reindex search engine", + "type": "shell", + "command": "docker compose run --rm web python manage.py reindex_search_engine_sounds && docker compose run --rm web python manage.py reindex_search_engine_forum", + "problemMatcher": [] + }, + { + "label": "Post dirty sounds to search engine", + "type": "shell", + "command": "docker compose run --rm web python manage.py post_dirty_sounds_to_search_engine", + "problemMatcher": [] + }, + { + "label": "Orchestrate analysis", + "type": "shell", + "command": "docker compose run --rm web python manage.py orchestrate_analysis", "problemMatcher": [] } ] diff --git a/freesound/settings.py b/freesound/settings.py index 2223ddd38..2b887bbd8 100644 --- a/freesound/settings.py +++ b/freesound/settings.py @@ -638,6 +638,16 @@ SOLR5_BASE_URL = "http://search:8983/solr" SOLR9_BASE_URL = "http://search:8983/solr" +SEARCH_ENGINE_SIMILARITY_ANALYZERS = { + FSDSINET_ANALYZER_NAME: { + 'vector_property_name': 'embeddings', + 'vector_size': 100, + } +} +SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER = FREESOUND_ESSENTIA_EXTRACTOR_NAME +SEARCH_ENGINE_NUM_SIMILAR_SOUNDS_PER_QUERY = 500 +USE_SEARCH_ENGINE_SIMILARITY = False + # ------------------------------------------------------------------------------- # Similarity client settings SIMILARITY_ADDRESS = 'similarity' diff --git a/general/tasks.py b/general/tasks.py index 2dea3c913..d7e0209d6 100644 --- a/general/tasks.py +++ b/general/tasks.py @@ -260,10 +260,12 @@ def process_analysis_results(sound_id, analyzer, status, analysis_time, exceptio {'task_name': PROCESS_ANALYSIS_RESULTS_TASK_NAME, 'sound_id': sound_id, 'analyzer': analyzer, 'status': status, 'exception': str(exception), 'work_time': round(time.time() - start_time)})) else: - # Load analysis output to database field (following configuration in settings.ANALYZERS_CONFIGURATION) + # Load analysis output to database field (following configuration in settings.ANALYZERS_CONFIGURATION) a.load_analysis_data_from_file_to_db() - # Set sound to index dirty so that the sound gets reindexed with updated analysis fields - a.sound.mark_index_dirty(commit=True) + + if analyzer in settings.SEARCH_ENGINE_SIMILARITY_ANALYZERS or analyzer in settings.ANALYZERS_CONFIGURATION: + # If the analyzer produces data that should be indexed in the search engine, set sound index to dirty so that the sound gets reindexed soon + a.sound.mark_index_dirty(commit=True) workers_logger.info("Finished processing analysis results (%s)" % json.dumps( {'task_name': PROCESS_ANALYSIS_RESULTS_TASK_NAME, 'sound_id': sound_id, 'analyzer': analyzer, 'status': status, 'work_time': round(time.time() - start_time)})) diff --git a/search/templatetags/search.py b/search/templatetags/search.py index 6f617de1c..5c2710b83 100644 --- a/search/templatetags/search.py +++ b/search/templatetags/search.py @@ -92,6 +92,8 @@ def display_facet(context, flt, facet, facet_type, title=""): context['sort'] if context['sort'] is not None else '', context['weights'] or '' ) + if context['similar_to'] is not None: + element['add_filter_url'] += '&similar_to={}'.format(context['similar_to']) filtered_facet.append(element) # We sort the facets by count. Also, we apply an opacity filter on "could" type pacets diff --git a/search/tests.py b/search/tests.py index 8f76da1e0..388a55337 100644 --- a/search/tests.py +++ b/search/tests.py @@ -20,7 +20,7 @@ from django.core.cache import cache from django.test import TestCase -from django.test.utils import skipIf +from django.test.utils import skipIf, override_settings from django.urls import reverse from sounds.models import Sound from utils.search import SearchResults, SearchResultsPaginator @@ -142,6 +142,7 @@ def test_search_page_response_ok(self, perform_search_engine_query): self.assertEqual(resp.context['error_text'], None) self.assertEqual(len(resp.context['docs']), self.NUM_RESULTS) + @mock.patch('search.views.perform_search_engine_query') def test_search_page_num_queries(self, perform_search_engine_query): perform_search_engine_query.return_value = self.perform_search_engine_query_response @@ -155,16 +156,32 @@ def test_search_page_num_queries(self, perform_search_engine_query): cache.clear() with self.assertNumQueries(1): self.client.get(reverse('sounds-search') + '?cm=1') - - # Now check number of queries when displaying results as packs (i.e., searching for packs) - cache.clear() - with self.assertNumQueries(5): - self.client.get(reverse('sounds-search') + '?only_p=1') - - # Also check packs when displaying in grid mode - cache.clear() - with self.assertNumQueries(5): - self.client.get(reverse('sounds-search') + '?only_p=1&cm=1') + + with override_settings(USE_SEARCH_ENGINE_SIMILARITY=True): + # When using search engine similarity, there'll be one extra query performed to get the similarity status of the sounds + + # Now check number of queries when displaying results as packs (i.e., searching for packs) + cache.clear() + with self.assertNumQueries(6): + self.client.get(reverse('sounds-search') + '?only_p=1') + + # Also check packs when displaying in grid mode + cache.clear() + with self.assertNumQueries(6): + self.client.get(reverse('sounds-search') + '?only_p=1&cm=1') + + with override_settings(USE_SEARCH_ENGINE_SIMILARITY=False): + # When not using search engine similarity, there'll be one less query performed as similarity state is retrieved directly from sound object + + # Now check number of queries when displaying results as packs (i.e., searching for packs) + cache.clear() + with self.assertNumQueries(5): + self.client.get(reverse('sounds-search') + '?only_p=1') + + # Also check packs when displaying in grid mode + cache.clear() + with self.assertNumQueries(5): + self.client.get(reverse('sounds-search') + '?only_p=1&cm=1') @mock.patch('search.views.perform_search_engine_query') def test_search_page_with_filters(self, perform_search_engine_query): diff --git a/search/views.py b/search/views.py index 79ea71928..8d182d66d 100644 --- a/search/views.py +++ b/search/views.py @@ -131,6 +131,7 @@ def search_view_helper(request, tags_mode=False): 'filter_query': query_params['query_filter'], 'filter_query_split': filter_query_split, 'search_query': query_params['textual_query'], + 'similar_to': query_params['similar_to'], 'group_by_pack_in_request': "1" if group_by_pack_in_request else "", 'disable_group_by_pack_option': disable_group_by_pack_option, 'only_sounds_with_pack': only_sounds_with_pack, @@ -152,7 +153,6 @@ def search_view_helper(request, tags_mode=False): 'has_advanced_search_settings_set': contains_active_advanced_search_filters(request, query_params, extra_vars), 'advanced_search_closed_on_load': settings.ADVANCED_SEARCH_MENU_ALWAYS_CLOSED_ON_PAGE_LOAD } - tvars.update(advanced_search_params_dict) try: @@ -205,7 +205,8 @@ def search_view_helper(request, tags_mode=False): # sure to remove the filters for the corresponding facet field thar are already active (so we remove # redundant information) if tags_in_filter: - results.facets['tag'] = [(tag, count) for tag, count in results.facets['tag'] if tag not in tags_in_filter] + if 'tag' in results.facets: + results.facets['tag'] = [(tag, count) for tag, count in results.facets['tag'] if tag not in tags_in_filter] tvars.update({ 'paginator': paginator, diff --git a/sounds/models.py b/sounds/models.py index ed47de7f4..10f139e02 100644 --- a/sounds/models.py +++ b/sounds/models.py @@ -64,7 +64,7 @@ from utils.mail import send_mail_template from utils.search import get_search_engine, SearchEngineException from utils.search.search_sounds import delete_sounds_from_search_engine -from utils.similarity_utilities import delete_sound_from_gaia +from utils.similarity_utilities import delete_sound_from_gaia, get_similarity_search_target_vector from utils.sound_upload import get_csv_lines, validate_input_csv_file, bulk_describe_from_csv web_logger = logging.getLogger('web') @@ -412,9 +412,15 @@ def get_analyzers_data_left_join_sql(self): def get_analysis_state_essentia_exists_sql(self): """Returns the SQL bits to add analysis_state_essentia_exists to the returned data indicating if thers is a - SoundAnalysis objects existing for th given sound_id for the essentia analyzer and with status OK""" + SoundAnalysis objects existing for the given sound_id for the essentia analyzer and with status OK""" return f" exists(select 1 from sounds_soundanalysis where sounds_soundanalysis.sound_id = sound.id AND sounds_soundanalysis.analyzer = '{settings.FREESOUND_ESSENTIA_EXTRACTOR_NAME}' AND sounds_soundanalysis.analysis_status = 'OK') as analysis_state_essentia_exists," + def get_search_engine_similarity_state_sql(self): + """Returns the SQL bits to add search_engine_similarity_state to the returned data indicating if thers is a + SoundAnalysis object existing for the default similarity analyzer (settings.SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER) + given sound_id and with status OK""" + return f" exists(select 1 from sounds_soundanalysis where sounds_soundanalysis.sound_id = sound.id AND sounds_soundanalysis.analyzer = '{settings.SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER}' AND sounds_soundanalysis.analysis_status = 'OK') as search_engine_similarity_state," + def bulk_query_solr(self, sound_ids): """For each sound, get all fields needed to index the sound in Solr. Using this custom query to avoid the need of having to do some extra queries when displaying some fields related to the sound (e.g. for tags). Using this @@ -514,6 +520,7 @@ def bulk_query(self, where, order_by, limit, args, include_analyzers_output=Fals accounts_profile.has_avatar as user_has_avatar, %s %s + %s ARRAY( SELECT tags_tag.name FROM tags_tag @@ -530,7 +537,8 @@ def bulk_query(self, where, order_by, limit, args, include_analyzers_output=Fals LEFT JOIN tickets_ticket ON tickets_ticket.sound_id = sound.id %s LEFT OUTER JOIN sounds_remixgroup_sounds ON sounds_remixgroup_sounds.sound_id = sound.id - WHERE %s """ % (self.get_analysis_state_essentia_exists_sql(), + WHERE %s """ % (self.get_search_engine_similarity_state_sql(), + self.get_analysis_state_essentia_exists_sql(), self.get_analyzers_data_select_sql() if include_analyzers_output else '', ContentType.objects.get_for_model(Sound).id, self.get_analyzers_data_left_join_sql() if include_analyzers_output else '', @@ -1350,6 +1358,24 @@ def get_geotag_name(self): return f'{self.geotag_lat:.2f}, {self.geotag_lon:.3f}' else: return f'{self.geotag.lat:.2f}, {self.geotag.lon:.3f}' + + @property + def ready_for_similarity(self): + # Retruns True is the sound has been analyzed for similarity and should be available for simialrity queries + if settings.USE_SEARCH_ENGINE_SIMILARITY: + if hasattr(self, 'search_engine_similarity_state'): + # If attribute is precomputed from query (because Sound was retrieved using bulk_query), no need to perform extra queries + return self.search_engine_similarity_state + else: + # Otherwise, check if there is a SoundAnalysis object for this sound with the correct analyzer and status + return SoundAnalysis.objects.filter(sound_id=self.id, analyzer=settings.SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER, analysis_status='OK').exists() + else: + # If not using search engine based similarity, then use the old similarity_state DB field + return self.similarity_state == "OK" + + def get_similarity_search_target_vector(self, analyzer=settings.SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER): + # If the sound has been analyzed for similarity, returns the vector to be used for similarity search + return get_similarity_search_target_vector(self.id, analyzer=analyzer) class Meta: ordering = ("-created", ) @@ -1577,7 +1603,7 @@ def bulk_query_id(self, pack_ids, sound_ids_for_pack_id=dict(), exclude_deleted= selected_sounds_data.append({ 'id': s.id, 'username': p.user.username, # Packs have same username as sounds inside pack - 'similarity_state': s.similarity_state, + 'ready_for_similarity': s.similarity_state == "OK" if not settings.USE_SEARCH_ENGINE_SIMILARITY else None, # If using search engine similarity, this needs to be retrieved later (see below) 'duration': s.duration, 'preview_mp3': s.locations('preview.LQ.mp3.url'), 'preview_ogg': s.locations('preview.LQ.ogg.url'), @@ -1585,7 +1611,7 @@ def bulk_query_id(self, pack_ids, sound_ids_for_pack_id=dict(), exclude_deleted= 'spectral': s.locations('display.spectral_bw.L.url'), 'num_ratings': s.num_ratings, 'avg_rating': s.avg_rating - }) + }) p.num_sounds_unpublished_precomputed = p.sounds.count() - p.num_sounds p.licenses_data_precomputed = ([lid for _, lid in licenses], [lname for lname, _ in licenses]) p.pack_tags = [{'name': tag, 'count': count, 'browse_url': p.browse_pack_tag_url(tag)} @@ -1596,6 +1622,16 @@ def bulk_query_id(self, pack_ids, sound_ids_for_pack_id=dict(), exclude_deleted= p.num_ratings_precomputed = len(ratings) p.avg_rating_precomputed = sum(ratings) / len(ratings) if len(ratings) else 0.0 + if settings.USE_SEARCH_ENGINE_SIMILARITY: + # To save an individual query for each selected sound, we get the similarity state of all selected sounds per pack in one single extra query + selected_sounds_ids = [] + for p in packs: + selected_sounds_ids += [s['id'] for s in p.selected_sounds_data] + sound_ids_ready_for_similarity = SoundAnalysis.objects.filter(sound_id__in=selected_sounds_ids, analyzer=settings.SEARCH_ENGINE_DEFAULT_SIMILARITY_ANALYZER, analysis_status="OK").values_list('sound_id', flat=True) + for p in packs: + for s in p.selected_sounds_data: + s['ready_for_similarity'] = s['id'] in sound_ids_ready_for_similarity + return packs def dict_ids(self, pack_ids, exclude_deleted=True): diff --git a/sounds/templatetags/display_sound.py b/sounds/templatetags/display_sound.py index 71cf40499..4d9cdbd70 100644 --- a/sounds/templatetags/display_sound.py +++ b/sounds/templatetags/display_sound.py @@ -200,7 +200,7 @@ def display_sound_no_sound_object(context, file_data, player_size, show_bookmark 'spectral': sound.locations('display.spectral_bw.L.url'), 'id': sound.id, # Only used for sounds that do actually have a sound object so we can display bookmark/similarity buttons 'username': sound.user.username, # Only used for sounds that do actually have a sound object so we can display bookmark/similarity/remix buttons - 'similarity_state': sound.similarity_state # Only used for sounds that do actually have a sound object so we can display bookmark/similarity/remix buttons + 'ready_for_similarity': sound.ready_for_similarity # Only used for sounds that do actually have a sound object so we can display bookmark/similarity/remix buttons 'remixgroup_id': sound.remixgroup_id # Only used for sounds that do actually have a sound object so we can display bookmark/similarity/remix buttons 'num_ratings': sound.num_ratings, # Used to display rating widget in players 'avg_rating': sound.avg_rating, # Used to display rating widget in players @@ -210,7 +210,7 @@ def display_sound_no_sound_object(context, file_data, player_size, show_bookmark 'sound': { 'id': file_data.get('id', file_data['preview_mp3'].split('/')[-2]), # If no id, use a unique fake ID to avoid caching problems 'username': file_data.get('username', 'nousername'), - 'similarity_state': file_data.get('similarity_state', 'FA'), + 'ready_for_similarity': file_data.get('ready_for_similarity', False), 'duration': file_data['duration'], 'samplerate': file_data.get('samplerate', 44100), 'num_ratings': file_data.get('num_ratings', 0), @@ -236,7 +236,7 @@ def display_sound_no_sound_object(context, file_data, player_size, show_bookmark }, 'show_milliseconds': 'true' if ('big' in player_size ) else 'false', 'show_bookmark_button': show_bookmark and 'id' in file_data, - 'show_similar_sounds_button': show_similar_sounds and 'similarity_state' in file_data, + 'show_similar_sounds_button': show_similar_sounds and file_data.get('ready_for_similarity', False), 'show_remix_group_button': show_remix and 'remixgroup_id' in file_data, 'show_rate_widget': 'avg_rating' in file_data, 'player_size': player_size, diff --git a/sounds/tests/test_sound.py b/sounds/tests/test_sound.py index 889b82ca5..5b6b411fa 100644 --- a/sounds/tests/test_sound.py +++ b/sounds/tests/test_sound.py @@ -793,6 +793,7 @@ def _test_similarity_update(self, cache_keys, expected, request_func, similarity self.assertEqual(self.sound.similarity_state, 'OK') self.assertContains(request_func(user) if user is not None else request_func(), expected) + @override_settings(USE_SEARCH_ENGINE_SIMILARITY=False) def test_similarity_update_display(self): self._test_similarity_update( self._get_sound_display_cache_keys(), @@ -801,6 +802,7 @@ def test_similarity_update_display(self): user=self.user, ) + @override_settings(USE_SEARCH_ENGINE_SIMILARITY=False) def test_similarity_update_view(self): self._test_similarity_update( self._get_sound_view_footer_top_cache_keys(), diff --git a/sounds/views.py b/sounds/views.py index 34ff14fdf..0a8c67596 100644 --- a/sounds/views.py +++ b/sounds/views.py @@ -65,6 +65,7 @@ from utils.nginxsendfile import sendfile, prepare_sendfile_arguments_for_sound_download from utils.pagination import paginate from utils.ratelimit import key_for_ratelimiting, rate_per_ip +from utils.search import get_search_engine, SearchEngineException from utils.search.search_sounds import get_random_sound_id_from_search_engine, perform_search_engine_query from utils.similarity_utilities import get_similar_sounds from utils.sound_upload import create_sound, NoAudioException, AlreadyExistsException, CantMoveException, \ @@ -820,13 +821,25 @@ def similar(request, username, sound_id): sound = get_object_or_404(Sound, id=sound_id, moderation_state="OK", - processing_state="OK", - similarity_state="OK") + processing_state="OK") if sound.user.username.lower() != username.lower(): raise Http404 - similarity_results, _ = get_similar_sounds( - sound, request.GET.get('preset', None), settings.NUM_SIMILAR_SOUNDS_PER_PAGE * settings.NUM_SIMILAR_SOUNDS_PAGES) + if not settings.USE_SEARCH_ENGINE_SIMILARITY: + # Get similar sounds from similarity service (gaia) + similarity_results, _ = get_similar_sounds( + sound, request.GET.get('preset', None), settings.NUM_SIMILAR_SOUNDS_PER_PAGE * settings.NUM_SIMILAR_SOUNDS_PAGES) + else: + # Get similar sounds from solr + try: + results = get_search_engine().search_sounds(similar_to=sound.id, + similar_to_max_num_sounds=settings.NUM_SIMILAR_SOUNDS_PER_PAGE * settings.NUM_SIMILAR_SOUNDS_PAGES, + num_sounds=settings.NUM_SIMILAR_SOUNDS_PER_PAGE * settings.NUM_SIMILAR_SOUNDS_PAGES) + similarity_results = [(result['id'], result['score']) for result in results.docs] + except SearchEngineException: + # Search engine not available, return empty list + similarity_results = [] + paginator = paginate(request, [sound_id for sound_id, _ in similarity_results], settings.NUM_SIMILAR_SOUNDS_PER_PAGE) similar_sounds = Sound.objects.ordered_ids(paginator['page'].object_list) tvars = {'similar_sounds': similar_sounds, 'sound': sound} diff --git a/templates/search/search.html b/templates/search/search.html index 018b734cb..9e2988d7e 100644 --- a/templates/search/search.html +++ b/templates/search/search.html @@ -36,9 +36,10 @@