|
| 1 | +""" |
| 2 | +Full-text-search and fuzzy-match GIN indexes for global search. |
| 3 | +
|
| 4 | +Adds, on the ten models the Pro global search (``pro/search/``) queries: |
| 5 | +
|
| 6 | +* one weighted ``tsvector`` GIN index per model, built from the same |
| 7 | + ``SearchVector`` objects the query annotates with, so the index expression |
| 8 | + can never drift from the compiled SQL across Django upgrades; and |
| 9 | +* one ``gin_trgm_ops`` index on each model's short display column for the |
| 10 | + fuzzy ``%>`` (``__trigram_word_similar``) lookup. |
| 11 | +
|
| 12 | +Each index is declared on its model's ``Meta.indexes``; this migration is the |
| 13 | +paired ``AddIndexConcurrently`` that actually builds them (the same house |
| 14 | +style as the other functional-index migrations, e.g. 0273). The tsvector |
| 15 | +indexes are built from the same ``SearchVector`` objects the query annotates |
| 16 | +with, so the index expression cannot drift from the compiled SQL across Django |
| 17 | +upgrades. The trigram indexes use ``opclasses`` (a base ``Index`` option), so |
| 18 | +they need no ``OpClass`` expression. |
| 19 | +
|
| 20 | +``AddIndexConcurrently`` / ``CREATE EXTENSION`` cannot run inside a |
| 21 | +transaction, hence ``atomic = False``. ``TrigramExtension`` is the sole |
| 22 | +creator of ``pg_trgm`` here, so its symmetric reverse (dropping the extension |
| 23 | +after the trigram indexes are already gone) is correct. |
| 24 | +""" |
| 25 | + |
| 26 | +from django.contrib.postgres.indexes import GinIndex |
| 27 | +from django.contrib.postgres.operations import AddIndexConcurrently, TrigramExtension |
| 28 | +from django.contrib.postgres.search import SearchVector |
| 29 | +from django.db import migrations |
| 30 | + |
| 31 | +# (model_name, ((column, weight), ...), index_name) -- weighted tsvector GIN. |
| 32 | +_FTS_SPECS = ( |
| 33 | + ("finding", (("title", "A"), ("description", "B")), "dojo_finding_fts_gin"), |
| 34 | + ("product", (("name", "A"), ("description", "B")), "dojo_product_fts_gin"), |
| 35 | + ("product_type", (("name", "A"), ("description", "B")), "dojo_product_type_fts_gin"), |
| 36 | + ("engagement", (("name", "A"), ("description", "B")), "dojo_engagement_fts_gin"), |
| 37 | + ("test", (("title", "A"), ("description", "B")), "dojo_test_fts_gin"), |
| 38 | + ("endpoint", (("host", "A"), ("path", "B")), "dojo_endpoint_fts_gin"), |
| 39 | + ("location", (("location_value", "A"), ("location_type", "B")), "dojo_location_fts_gin"), |
| 40 | + ("finding_template", (("title", "A"), ("description", "B")), "dojo_finding_template_fts_gin"), |
| 41 | + ("app_analysis", (("name", "A"),), "dojo_app_analysis_fts_gin"), |
| 42 | + ("vulnerability_id", (("vulnerability_id", "A"),), "dojo_vulnerability_id_fts_gin"), |
| 43 | +) |
| 44 | + |
| 45 | +# (model_name, column, index_name) -- gin_trgm_ops fuzzy-match index. Three |
| 46 | +# names are abbreviated to stay within the 30-char index-name limit (E033): |
| 47 | +# location_value -> locval, finding_template -> findtmpl, vulnerability_id -> vuln_id. |
| 48 | +_TRGM_SPECS = ( |
| 49 | + ("finding", "title", "dojo_finding_title_trgm"), |
| 50 | + ("product", "name", "dojo_product_name_trgm"), |
| 51 | + ("product_type", "name", "dojo_product_type_name_trgm"), |
| 52 | + ("engagement", "name", "dojo_engagement_name_trgm"), |
| 53 | + ("test", "title", "dojo_test_title_trgm"), |
| 54 | + ("endpoint", "host", "dojo_endpoint_host_trgm"), |
| 55 | + ("location", "location_value", "dojo_location_locval_trgm"), |
| 56 | + ("finding_template", "title", "dojo_findtmpl_title_trgm"), |
| 57 | + ("app_analysis", "name", "dojo_app_analysis_name_trgm"), |
| 58 | + ("vulnerability_id", "vulnerability_id", "dojo_vuln_id_trgm"), |
| 59 | +) |
| 60 | + |
| 61 | + |
| 62 | +def _fts_vector(fields): |
| 63 | + vector = None |
| 64 | + for column, weight in fields: |
| 65 | + component = SearchVector(column, weight=weight, config="english") |
| 66 | + vector = component if vector is None else vector + component |
| 67 | + return vector |
| 68 | + |
| 69 | + |
| 70 | +def _index_operations(): |
| 71 | + add_index = [] |
| 72 | + for model_name, fields, name in _FTS_SPECS: |
| 73 | + add_index.append( |
| 74 | + AddIndexConcurrently( |
| 75 | + model_name=model_name, |
| 76 | + index=GinIndex(_fts_vector(fields), name=name), |
| 77 | + ), |
| 78 | + ) |
| 79 | + for model_name, column, name in _TRGM_SPECS: |
| 80 | + add_index.append( |
| 81 | + AddIndexConcurrently( |
| 82 | + model_name=model_name, |
| 83 | + index=GinIndex(fields=[column], opclasses=["gin_trgm_ops"], name=name), |
| 84 | + ), |
| 85 | + ) |
| 86 | + return [TrigramExtension(), *add_index] |
| 87 | + |
| 88 | + |
| 89 | +class Migration(migrations.Migration): |
| 90 | + atomic = False |
| 91 | + |
| 92 | + dependencies = [ |
| 93 | + ("dojo", "0277_seed_deduplication_execution_mode"), |
| 94 | + ] |
| 95 | + |
| 96 | + operations = _index_operations() |
0 commit comments