@@ -13,6 +13,91 @@ class HadithService:
1313 def __init__ (self , repository : HadithRepository ):
1414 self .repository = repository
1515
16+ def _build_get_by_id_pipeline (
17+ self , hadith_id : ObjectId , languages : list [str ]
18+ ) -> list [dict [str , Any ]]:
19+ return [
20+ {"$match" : {"_id" : hadith_id }},
21+ # 🔹 Edition lookup
22+ {
23+ "$lookup" : {
24+ "from" : "edition" ,
25+ "let" : {"id" : "$editionId" },
26+ "pipeline" : [
27+ {"$match" : {"$expr" : {"$eq" : ["$_id" , "$$id" ]}}},
28+ {
29+ "$project" : {
30+ "_id" : 1 ,
31+ "slug" : 1 ,
32+ "availableLanguages" : 1 ,
33+ "hadithCount" : 1 ,
34+ "bookCount" : 1 ,
35+ "name" : self ._filter_languages_expr (languages , "$name" ),
36+ }
37+ },
38+ ],
39+ "as" : "edition" ,
40+ }
41+ },
42+ {"$unwind" : {"path" : "$edition" , "preserveNullAndEmptyArrays" : True }},
43+ # 🔹 Book lookup
44+ {
45+ "$lookup" : {
46+ "from" : "book" ,
47+ "let" : {
48+ "editionId" : "$editionId" ,
49+ "bookIndex" : "$bookIndex" ,
50+ },
51+ "pipeline" : [
52+ {
53+ "$match" : {
54+ "$expr" : {
55+ "$and" : [
56+ {"$eq" : ["$editionId" , "$$editionId" ]},
57+ {"$eq" : ["$bookIndex" , "$$bookIndex" ]},
58+ ]
59+ }
60+ }
61+ },
62+ {
63+ "$project" : {
64+ "_id" : 1 ,
65+ "slug" : 1 ,
66+ "editionId" : 1 ,
67+ "bookIndex" : 1 ,
68+ "hadithCount" : 1 ,
69+ "hadithIndexStart" : 1 ,
70+ "name" : self ._filter_languages_expr (languages , "$name" ),
71+ }
72+ },
73+ ],
74+ "as" : "book" ,
75+ }
76+ },
77+ {"$unwind" : {"path" : "$book" , "preserveNullAndEmptyArrays" : True }},
78+ # 🔹 Final projection (reuse your logic)
79+ {
80+ "$project" : {
81+ ** self ._projection_for_languages (languages ),
82+ # remove nulls cleanly
83+ "edition" : {
84+ "$cond" : [
85+ {"$ne" : ["$edition" , None ]},
86+ "$edition" ,
87+ "$$REMOVE" ,
88+ ]
89+ },
90+ "book" : {
91+ "$cond" : [
92+ {"$ne" : ["$book" , None ]},
93+ "$book" ,
94+ "$$REMOVE" ,
95+ ]
96+ },
97+ }
98+ },
99+ ]
100+
16101 def _projection_for_languages (self , languages : list [str ]) -> dict [str , int ]:
17102 projection = {
18103 "_id" : 1 ,
@@ -29,12 +114,26 @@ def _projection_for_languages(self, languages: list[str]) -> dict[str, int]:
29114 projection .update (build_text_projection (languages ))
30115 return projection
31116
117+ def _filter_languages_expr (self , languages : list [str ], field : str ) -> Any :
118+ if "*" in languages :
119+ return field
120+
121+ return {
122+ "$arrayToObject" : {
123+ "$filter" : {
124+ "input" : {"$objectToArray" : field },
125+ "as" : "item" ,
126+ "cond" : {"$in" : ["$$item.k" , languages ]},
127+ }
128+ }
129+ }
130+
32131 def _search_text_projection (
33- self , languages : list [str ]
132+ self , languages : list [str ], key : str = "$text"
34133 ) -> dict [str , Any ] | Literal [1 ]:
35134 if "*" in languages :
36135 return 1
37- return {language : f"$text .{ language } " for language in languages }
136+ return {language : f"{ key } .{ language } " for language in languages }
38137
39138 def _build_search_pipeline (
40139 self ,
@@ -77,6 +176,8 @@ def _build_search_pipeline(
77176 "text" : self ._search_text_projection (languages ),
78177 "grades" : 1 ,
79178 "score" : {"$meta" : "searchScore" },
179+ "edition" : 1 ,
180+ "book" : 1 ,
80181 }
81182 }
82183
@@ -88,6 +189,62 @@ def _build_search_pipeline(
88189 }
89190 },
90191 {"$limit" : 100 },
192+ # 👇 ADD LOOKUPS HERE
193+ {
194+ "$lookup" : {
195+ "from" : "edition" ,
196+ "let" : {"id" : "$editionId" },
197+ "pipeline" : [
198+ {"$match" : {"$expr" : {"$eq" : ["$_id" , "$$id" ]}}},
199+ {
200+ "$project" : {
201+ "_id" : 1 ,
202+ "availableLanguages" : 1 ,
203+ "slug" : 1 ,
204+ "hadithCount" : 1 ,
205+ "bookCount" : 1 ,
206+ "name" : self ._filter_languages_expr (languages , "$name" ),
207+ }
208+ },
209+ ],
210+ "as" : "edition" ,
211+ }
212+ },
213+ {"$unwind" : {"path" : "$edition" , "preserveNullAndEmptyArrays" : True }},
214+ {
215+ "$lookup" : {
216+ "from" : "book" ,
217+ "let" : {
218+ "editionId" : "$editionId" ,
219+ "bookIndex" : "$bookIndex" ,
220+ },
221+ "pipeline" : [
222+ {
223+ "$match" : {
224+ "$expr" : {
225+ "$and" : [
226+ {"$eq" : ["$editionId" , "$$editionId" ]},
227+ {"$eq" : ["$bookIndex" , "$$bookIndex" ]},
228+ ]
229+ }
230+ }
231+ },
232+ {
233+ "$project" : {
234+ "_id" : 1 ,
235+ "slug" : 1 ,
236+ "editionId" : 1 ,
237+ "hadithCount" : 1 ,
238+ "bookIndex" : 1 ,
239+ "hadithIndexStart" : 1 ,
240+ "name" : self ._filter_languages_expr (languages , "$name" ),
241+ }
242+ },
243+ ],
244+ "as" : "book" ,
245+ }
246+ },
247+ {"$unwind" : {"path" : "$book" , "preserveNullAndEmptyArrays" : True }},
91248 {
92249 "$facet" : {
93250 "items" : [
@@ -112,10 +269,13 @@ def _build_search_pipeline(
112269 def get_hadith_by_id (
113270 self , hadith_id : PyObjectId , languages : list [str ]
114271 ) -> dict [str , Any ]:
115- projection = self ._projection_for_languages (languages )
116- hadith = self .repository .find_one_by_id (ObjectId (hadith_id ), projection )
272+ pipeline = self ._build_get_by_id_pipeline (ObjectId (hadith_id ), languages )
273+
274+ hadith = self .repository .find_one_with_lookup (pipeline )
275+
117276 if hadith is None :
118277 raise HadithNotFoundError (hadith_id )
278+
119279 return hadith
120280
121281 def get_hadiths_paginated (
0 commit comments