Skip to content

Commit 54e1cdd

Browse files
Merge pull request #441 from open5e/v2_search_improved
V2 search improved
2 parents 81d551b + 002dd46 commit 54e1cdd

File tree

10 files changed

+191
-70
lines changed

10 files changed

+191
-70
lines changed

api_v2/management/commands/index_v1.py renamed to api_v2/management/commands/buildindex.py

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,72 @@ def unload_all_content(self):
1818
v2.SearchResult.objects.all().delete()
1919
print("UNLOADED_OBJECT_COUNT:{}".format(object_count))
2020

21-
22-
def load_content(self,model,schema):
23-
print("SCHEMA:{} OBJECT_COUNT:{} MODEL:{} TABLE_NAME:{}".format(
24-
schema,
25-
model.objects.all().count(),
26-
model.__name__,
27-
model._meta.db_table))
28-
21+
def load_v1_content(self, model):
22+
results = []
2923
standard_v1_models = ['MagicItem','Spell','Monster','CharClass','Archetype',
30-
'Race','Subrace','Plane','Section','Feat','Condition','Background','Weapon','Armor']
31-
32-
search_results = []
24+
'Race','Subrace','Plane','Section','Feat','Condition','Background','Weapon','Armor']
3325

34-
if model.__name__ in standard_v1_models and schema=='v1':
26+
if model.__name__ in standard_v1_models:
3527
for o in model.objects.all():
36-
search_results.append(v2.SearchResult(
28+
results.append(v2.SearchResult(
3729
document_pk=o.document.slug,
38-
document_name=o.document.title,
3930
object_pk=o.slug,
4031
object_name=o.name,
41-
object_route=o.route,
32+
object_model=o.__class__.__name__,
4233
schema_version="v1",
4334
text=o.name+"\n"+o.desc
4435

4536
))
37+
return results
4638

47-
v2.SearchResult.objects.bulk_create(search_results)
39+
def load_v2_content(self, model):
40+
results = []
41+
standard_v2_models = ['Item','Spell','Creature','CharacterClass','Race','Feat','Condition','Background']
4842

43+
if model.__name__ in standard_v2_models:
44+
for o in model.objects.all():
45+
results.append(v2.SearchResult(
46+
document_pk=o.document.key,
47+
object_pk=o.pk,
48+
object_name=o.name,
49+
object_model=o.__class__.__name__,
50+
schema_version='v2',
51+
text=o.as_text()
52+
))
53+
return results
54+
55+
def load_content(self,model,schema):
56+
print("SCHEMA:{} OBJECT_COUNT:{} MODEL:{} TABLE_NAME:{}".format(
57+
schema,
58+
model.objects.all().count(),
59+
model.__name__,
60+
model._meta.db_table))
61+
62+
if schema == 'v1':
63+
v2.SearchResult.objects.bulk_create(
64+
self.load_v1_content(model)
65+
)
66+
67+
if schema == 'v2':
68+
v2.SearchResult.objects.bulk_create(
69+
self.load_v2_content(model)
70+
)
4971

5072
def load_index(self):
5173
with connection.cursor() as cursor:
5274

5375
cursor.execute("DROP TABLE IF EXISTS search_index;")
54-
55-
cursor.execute("CREATE VIRTUAL TABLE search_index USING FTS5(document_pk,document_name,object_pk,object_name,object_route,text,schema_version);")
5676

57-
cursor.execute("INSERT INTO search_index (document_pk,document_name,object_pk,object_name,object_route,text,schema_version) SELECT document_pk,document_name,object_pk,object_name,object_route,text,schema_version FROM api_v2_searchresult")
58-
77+
cursor.execute(
78+
"CREATE VIRTUAL TABLE search_index " +
79+
"USING FTS5(document_pk,object_pk,object_name,object_model,text,schema_version);")
80+
81+
cursor.execute(
82+
"INSERT INTO search_index " +
83+
"(document_pk,object_pk,object_name,object_model,text,schema_version) " +
84+
"SELECT document_pk,object_pk,object_name,object_model,text,schema_version " +
85+
"FROM api_v2_searchresult")
86+
5987

6088
def check_fts_enabled(self):
6189
#import sqlite3
@@ -91,6 +119,15 @@ def handle(self, *args, **options) -> None:
91119
self.load_content(v1.Weapon,"v1")
92120
self.load_content(v1.Armor,"v1")
93121

122+
self.load_content(v2.Item,"v2")
123+
self.load_content(v2.Spell,"v2")
124+
self.load_content(v2.Creature,"v2")
125+
self.load_content(v2.CharacterClass,"v2")
126+
self.load_content(v2.Race,"v2")
127+
self.load_content(v2.Feat,"v2")
128+
self.load_content(v2.Condition,"v2")
129+
self.load_content(v2.Background,"v2")
130+
94131

95132
# Take the content table's current data and load it into the index.
96133
self.load_index()
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 3.2.20 on 2024-04-03 00:10
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api_v2', '0072_merge_20240329_1829'),
10+
]
11+
12+
operations = [
13+
migrations.RenameField(
14+
model_name='searchresult',
15+
old_name='object_route',
16+
new_name='object_model',
17+
),
18+
migrations.RemoveField(
19+
model_name='searchresult',
20+
name='document_name',
21+
),
22+
]

api_v2/models/background.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ class Meta:
2727
"""To assist with the UI layer."""
2828

2929
verbose_name_plural = "backgrounds"
30+

api_v2/models/characterclass.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,12 @@ def __str__(self):
9797
if self.is_subclass:
9898
return "{} [{}]".format(self.subclass_of.name, self.name)
9999
else:
100-
return self.name
100+
return self.name
101+
102+
def as_text(self):
103+
text = self.name + '\n'
104+
105+
for feature in self.feature_set.all():
106+
text+='\n' + feature.as_text()
107+
108+
return text

api_v2/models/creature.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ class Creature(Object, Abilities, FromDocument):
5757
max_length=100,
5858
help_text='The creature\'s allowed alignments.'
5959
)
60+
61+
def as_text(self):
62+
text = self.name + '\n'
63+
64+
for action in self.creatureaction_set.all():
65+
text+='\n' + action.as_text()
66+
67+
return text
68+
69+
def search_result_extra_fields(self):
70+
return {
71+
"armor_class":self.armor_class,
72+
"hit_points":self.hit_points,
73+
"ability_scores":self.get_ability_scores(),
74+
}
6075

6176
@property
6277
def creatureset(self):

api_v2/models/document.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,16 @@ class FromDocument(models.Model):
112112
max_length=100,
113113
help_text="Unique key for the Item.")
114114

115+
def as_text(self):
116+
return "{}\n\n{}".format(self.name, self.desc)
117+
115118
def get_absolute_url(self):
116119
return reverse(self.__name__, kwargs={"pk": self.pk})
117120

121+
def search_result_extra_fields(self):
122+
return {
123+
"school":self.school.key,
124+
}
125+
118126
class Meta:
119127
abstract = True

api_v2/models/item.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ class Item(Object, HasDescription, FromDocument):
7070
def is_magic_item(self):
7171
return self.rarity is not None
7272

73+
def search_result_extra_fields(self):
74+
fields = {"type":self.category.key}
75+
if self.is_magic_item:
76+
fields["rarity"]=self.rarity.key
77+
return fields
78+
79+
7380

7481
class ItemSet(HasName, HasDescription, FromDocument):
7582
"""A set of items to be referenced."""

api_v2/models/search.py

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,11 @@ class SearchResult(models.Model):
55
""" The Search Result object model"""
66

77
document_pk = models.CharField(max_length=255)
8-
document_name = models.CharField(max_length=100)
98
object_pk = models.CharField(max_length=255)
109
object_name = models.CharField(max_length=100)
11-
object_route = models.CharField(max_length=255)
10+
object_model = models.CharField(max_length=255)
1211
schema_version = models.CharField(max_length=100)
1312

1413
rank = models.DecimalField(max_digits=10, decimal_places=4, null=True, default=None)
1514
text = models.TextField(null=True, default=None)
1615
highlighted = models.TextField(null=True, default=None)
17-
18-
@property
19-
def document_slug(self):
20-
return self.document_pk
21-
22-
@property
23-
def document_title(self):
24-
return self.document_name
25-
26-
@property
27-
def route(self):
28-
return self.object_route
29-
30-
@property
31-
def slug(self):
32-
return self.object_pk
33-
34-
@property
35-
def name(self):
36-
return self.object_name

api_v2/serializers/search.py

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,85 @@
44

55
from api_v2 import models
66
from api import models as v1
7+
from django.urls import reverse
78

89
class SearchResultSerializer(serializers.ModelSerializer):
9-
rank = serializers.ReadOnlyField()
10-
text = serializers.ReadOnlyField()
11-
highlighted = serializers.ReadOnlyField()
1210
object = serializers.SerializerMethodField(method_name='get_object')
11+
document = serializers.SerializerMethodField(method_name='get_document')
12+
route = serializers.SerializerMethodField(method_name='get_route')
1313

1414

1515
class Meta:
1616
model = models.SearchResult
1717
fields = [
18-
'document_pk',
19-
'document_name',
18+
'document',
2019
'object_pk',
2120
'object_name',
2221
'object',
23-
'object_route',
22+
'object_model',
2423
'schema_version',
24+
'route',
2525
'rank',
2626
'text',
2727
'highlighted']
2828

2929
def get_object(self, obj):
30+
result_detail = None
31+
3032
if obj.schema_version == 'v1':
31-
if obj.object_route == 'magicitems/':
33+
if obj.object_model == 'MagicItem':
3234
result_detail = v1.MagicItem.objects.get(slug=obj.object_pk)
33-
return result_detail.search_result_extra_fields()
34-
35-
if obj.object_route == 'monsters/':
35+
if obj.object_model == 'Monster':
3636
result_detail = v1.Monster.objects.get(slug=obj.object_pk)
37-
return result_detail.search_result_extra_fields()
38-
39-
if obj.object_route == 'spells/':
37+
if obj.object_model == 'Spell':
4038
result_detail = v1.Spell.objects.get(slug=obj.object_pk)
41-
return result_detail.search_result_extra_fields()
42-
43-
if obj.object_route == 'sections/':
39+
if obj.object_model == 'Section':
4440
result_detail = v1.Section.objects.get(slug=obj.object_pk)
45-
return result_detail.search_result_extra_fields()
41+
42+
if obj.schema_version == 'v2':
43+
if obj.object_model == 'Item':
44+
result_detail = models.Item.objects.get(pk=obj.object_pk)
45+
if obj.object_model == 'Creature':
46+
result_detail = models.Creature.objects.get(pk=obj.object_pk)
47+
if obj.object_model == 'Spell':
48+
result_detail = models.Spell.objects.get(pk=obj.object_pk)
49+
50+
if result_detail is not None:
51+
return result_detail.search_result_extra_fields()
52+
else:
53+
return None
54+
55+
def get_document(self, obj):
56+
if obj.schema_version == 'v1':
57+
doc = v1.Document.objects.get(slug=obj.document_pk)
58+
return {
59+
'key': doc.slug,
60+
'name': doc.title
61+
}
62+
63+
if obj.schema_version == 'v2':
64+
doc = models.Document.objects.get(key=obj.document_pk)
65+
return {
66+
'key': doc.key,
67+
'name': doc.name
68+
}
69+
70+
def get_route(self, obj):
71+
# May want to split this out into v1 and v2?
72+
route_lookup = {
73+
"Item":"items",
74+
"Creature":"creatures",
75+
"Spell":"spells",
76+
"CharacterClass":"class",
77+
"Monster":"monsters",
78+
"MagicItem":"magicitems",
79+
"Section":"sections",
80+
"Background":"backgrounds",
81+
"Subrace":"subraces",
82+
"Feat":"feats",
83+
"Race":"races",
84+
"Plane":"planes",
85+
}
86+
87+
route = "{}/{}/".format(obj.schema_version,route_lookup[obj.object_model])
88+
return route

api_v2/views/search.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,21 @@ def get_queryset(self):
3333
else:
3434
document_pk = self.request.query_params.get("document_pk")
3535

36-
if self.request.query_params.get("object_route") is None:
37-
object_route = '%'
36+
if self.request.query_params.get("object_model") is None:
37+
object_model = '%'
3838
else:
39-
object_route = self.request.query_params.get("object_route")
39+
object_model = self.request.query_params.get("object_model")
4040

41-
queryset = models.SearchResult.objects.raw(
41+
weighted_queryset = models.SearchResult.objects.raw(
4242
"SELECT 1 as id,rank, " +
43-
"snippet(search_index,5,'<span class=\"highlighted\">','</span>','...',20) as highlighted, " +
44-
"* FROM search_index " +
43+
"snippet(search_index,4,'<span class=\"highlighted\">','</span>','...',20) as highlighted, " +
44+
"document_pk,object_pk,object_name,object_model,text,schema_version FROM search_index " +
4545
"WHERE " +
4646
"schema_version LIKE %s " +
4747
"AND document_pk LIKE %s " +
48-
"AND object_route LIKE %s " +
49-
"AND text MATCH %s " +
50-
"ORDER BY rank",[schema_version, document_pk, object_route, query])
48+
"AND object_model LIKE %s " +
49+
"AND search_index MATCH %s" +
50+
"AND rank MATCH 'bm25(1.0, 1.0, 10.0)'"+ # This line results in a 10x weight to Name
51+
"ORDER BY rank",[schema_version, document_pk, object_model, query])
5152

52-
return queryset
53+
return weighted_queryset

0 commit comments

Comments
 (0)