package ucca import ( "bytes" "context" "encoding/json" "fmt" "net/http" ) // FetchByNormIDs loads one representative unit per norm_id from the KB slice // collection — the fetch side of the Concept->Norm recall injector. Returns // LegalSearchResult with the caller-provided concept-relevance score (there is no // similarity query; the injector places them by that score). Returns nil on any // error or when no KB slice is configured (graceful degradation). func (c *LegalRAGClient) FetchByNormIDs(ctx context.Context, normIDs []string, score float64) []LegalSearchResult { if c.kbSliceCollection == "" || len(normIDs) == 0 { return nil } should := make([]map[string]interface{}, 0, len(normIDs)) for _, nid := range normIDs { should = append(should, map[string]interface{}{"key": "norm_id", "match": map[string]interface{}{"value": nid}}) } reqBody := map[string]interface{}{ "limit": len(normIDs) * 3, "with_payload": true, "with_vectors": false, "filter": map[string]interface{}{"should": should}, } jsonBody, err := json.Marshal(reqBody) if err != nil { return nil } url := fmt.Sprintf("%s/collections/%s/points/scroll", c.qdrantURL, c.kbSliceCollection) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) if err != nil { return nil } req.Header.Set("Content-Type", "application/json") if c.qdrantAPIKey != "" { req.Header.Set("api-key", c.qdrantAPIKey) } resp, err := c.httpClient.Do(req) if err != nil { return nil } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil } var scrollResp qdrantScrollResponse if err := json.NewDecoder(resp.Body).Decode(&scrollResp); err != nil { return nil } seen := map[string]bool{} out := make([]LegalSearchResult, 0, len(normIDs)) for _, pt := range scrollResp.Result.Points { nid := getString(pt.Payload, "norm_id") if nid == "" || seen[nid] { continue } seen[nid] = true out = append(out, scrollPointToResult(pt.Payload, score)) } return out } // scrollPointToResult maps a scroll-point payload to a LegalSearchResult. Mirrors // hitsToResults' payload keys; the score is assigned by the caller (concept rank). func scrollPointToResult(payload map[string]interface{}, score float64) LegalSearchResult { regCode := getString(payload, "regulation_code") if regCode == "" { regCode = getString(payload, "regulation_id") } return LegalSearchResult{ Text: getString(payload, "chunk_text"), RegulationCode: regCode, RegulationName: getString(payload, "regulation_name_de"), RegulationShort: getString(payload, "regulation_short"), Category: getString(payload, "category"), Article: getString(payload, "article"), ArticleLabel: getString(payload, "article_label"), Paragraph: getString(payload, "paragraph"), SourceURL: getString(payload, "source_url"), CitationUnit: getString(payload, "citation_unit"), Score: score, } }