Site icon Blog ARC Optimizer

Série de chiffon sémantique Partie 4: Découverte de contenu

Série de chiffon sémantique Partie 4: Découverte de contenu


Ceci est la quatrième partie de notre série sur le chiffon sémantique, dans lequel nous vous montrons comment orchestrer un pipeline de requête multimodel pour découvrir les informations les plus pertinentes. Avant de plonger, assurez-vous de vérifier
Première partie: Améliorer le Genai avec des données et des graphiques de connaissances multimodèles,
Deuxième partie: le graphique de connaissanceset
Troisième partie: préparation du contenu.

La troisième étape de notre flux de travail de chiffon sémantique est la découverte de contenu. La découverte de contenu est le processus de recherche d’informations pertinentes dans un grand corpus de données. Une partie du processus de découverte de contenu consiste à déterminer les concepts clés de la question d’un utilisateur, à découvrir des morceaux d’informations pertinentes pertinentes à la question et à préparer une invite pour le LLM.

En interceptant la question de l’utilisateur, les concepts clés peuvent être extraits en utilisant la puissance des graphiques de connaissances, tout comme le flux de travail de préparation de contenu. Ces concepts guident ensuite une recherche basée sur la pertinence dans le corpus de texte pour localiser les informations les plus pertinentes. Pour améliorer la précision, les capacités vectorielles sont utilisées pour relancer les résultats et identifier un contenu sémantiquement similaire. Enfin, la préparation rapide consiste à intégrer ces morceaux organisés dans le contexte de la LLM, ce qui lui permet de tirer parti des connaissances propriétaires efficacement lors de la génération de réponses. Jetons un coup d’œil à ce processus étape par étape.

Découvrir le contenu et les Q&R Genai

Dans les exemples suivants, nous afficherons les extraits de code Java qui utilisent le
Progresser Marklogic,
Semaphore de progressionet Langchain4J Sdks. Cela nous permettra d’interagir avec les composants MarkLogic, Semaphore et Azure OpenAI. Vous pouvez passer ces appels dans une application de niveau intermédiaire qui expose une API pour vos utilisateurs finaux.

Image 1: flux de travail illustrant comment un utilisateur soumet une question, la question est analysée avec le sémaphore, il trouve du contenu dans MarkLogic, l’invite est préparée, puis elle est envoyée au LLM.

Comprendre l’intention de l’utilisateur

Bien que vous puissiez interagir avec le Genai directement en utilisant la question de l’utilisateur, nous intercepterons la question pour mieux comprendre leur intention. Le même processus de classification du sémaphore que nous avons utilisé plus tôt pour trouver les concepts clés de nos morceaux peut être utilisé pour trouver des concepts dans la question de l’utilisateur. Les étiquettes conceptuelles seront utilisées comme recherche lexicale pour trouver des éléments dans votre corpus de données – tandis que les identificateurs seront utilisés pour déterminer le contenu classifié en fonction des preuves à l’appui.

Nous allons d’abord créer un client de classification à utiliser sur notre application. Nous utilisons Spring Framework pour lire les valeurs à partir d’un fichier de propriétés d’application. Cependant, vous pouvez accéder à ces valeurs en utilisant la méthode que vous voyez.

TokenFetcher tokenFetcher = new TokenFetcher(this.semaphoreConfig.getTokenRequestURL(), this.semaphoreConfig.getKey());
Token token = tokenFetcher.getAccessToken();

ClassificationClient classificationClient = new ClassificationClient();
ClassificationConfiguration classificationConfiguration = new ClassificationConfiguration();
classificationConfiguration.setUrl(this.semaphoreConfig.getCsURL());
classificationConfiguration.setApiToken(token.getAccess_token());
classificationClient.setClassificationConfiguration(classificationConfiguration);

Une fois qu’un client de classification est établi, la création d’une demande de classification est facile. Cette demande contiendra le texte du morceau et un titre vide.

classificationClient.getClassifiedDocument(new Body(question, new Title(""));

La réponse à la sémaphore sera automatiquement rassemblée dans un objet Java pour vous. Vous pouvez accéder à tous les concepts à partir de ce résultat.

LOGGER.info("---------Start Concepts identified----------");
for (Map.Entry<String, Collection<ClassificationScore>> entry : concepts.entrySet()) {
    LOGGER.info(entry.getKey() + ":");
    for (ClassificationScore classificationScore : entry.getValue()) {
        LOGGER.info(String.format(" %s %s %s %f",
                classificationScore.getRulebaseClass(),
                classificationScore.getId(),
                classificationScore.getName(),
                classificationScore.getScore()
        ));
    }
}
LOGGER.info("---------Ending Concepts identified---------");

Avec ces concepts identifiés, nous pouvons commencer à construire une requête. Nous allons prendre chacun des concepts et construire une combinaison d’une recherche de mots lexicale et d’une recherche d’identifiant contre le contenu, donc le seul contenu qui a été classé est apparié.


Result result = classificationClient.getClassifiedDocument(new Body(query.text()), new Title(BLANK));

// Convert these concepts into a MarkLogic query to find relevant chunks.
List<CtsQueryExpr> queryExprList = new ArrayList<>();
for (Map.Entry<String, Collection<ClassificationScore>> entry : result.getAllClassifications().entrySet()) {
    for (ClassificationScore classificationScore : entry.getValue()) {
        queryExprList.add(op.cts.wordQuery(classificationScore.getName(), "case-insensitive", "punctuation-insensitive", "lang=en"));
        queryExprList.add(op.cts.jsonPropertyValueQuery("id", classificationScore.getId()));
    }
}

Trouver du contenu pertinent

Nous avons maintenant une requête bien structurée basée sur l’intention de l’utilisateur. Ensuite, nous créerons une requête composable pour trouver les morceaux pertinents avec l’API optique MarkLogic. Il peut rechercher toutes les formes de données stockées dans
Serveur marklogique. Par défaut, MarkLogic Server utilise un algorithme de notation de la fréquence des termes / fréquence de document inverse (TF-IDF) pour marquer et commander les résultats.


// Convert these concepts into a MarkLogic query to find relevant chunks.
List<CtsQueryExpr> queryExprList = new ArrayList<>();
for (Map.Entry<String, Collection<ClassificationScore>> entry : result.getAllClassifications().entrySet()) {
        for (ClassificationScore classificationScore : entry.getValue()) {
        queryExprList.add(op.cts.wordQuery(classificationScore.getName(), "case-insensitive", "punctuation-insensitive));
        queryExprList.add(op.cts.jsonPropertyValueQuery("id", classificationScore.getId()));
        }
}

// Build the query to only look at chunks of documents that have been loaded.
CtsQueryExpr andQuery = op.cts.andQuery(
        op.cts.collectionQuery(DocumentConstants.COLLECTION_CHUNK),
        op.cts.orQuery(queryExprList.toArray(CtsQueryExpr[]::new))
);

// The following query plan searches for the matching fragments utilizing the key concepts that have been
// extracted from the knowledge graph.
PlanBuilder.ModifyPlan plan =
        op.fromSearchDocs(andQuery)
                .orderBy(op.desc(op.col("score")))
                .limit(10);

Comme vous le voyez dans la requête ci-dessus, nous utilisons une recherche basée sur la pertinence. MarkLogic Server 12 a introduit deux nouvelles fonctionnalités pour aider à la recherche.
Meilleur match 25 (BM25) et recherche vectorielle nativesont disponibles pour améliorer votre expérience.

BM25 est un algorithme de classement pertinent comme TF-IDF utilisé dans les requêtes lexicales. BM25 prend en considération plusieurs facettes telles que la fréquence des termes et la longueur du document. BM25 a également un paramètre accordable pour le poids lors de la prise en compte de la longueur du document.

La recherche vectorielle vous permet d’interroger les intégres vectoriels stockés dans MarkLogic Server. Pour prendre en charge la recherche de vecteur, MarkLogic Server a introduit un index vectoriel spécialisé avec la recherche Ann (approximativement le plus proche du voisin). Ann est un moyen rapide et efficace de trouver du contenu en fonction de la similitude des vecteurs.

Dans cet exemple, la recherche vectorielle est utilisée avec la recherche lexicale. Nous pouvons prendre une vectorisation de la question de l’utilisateur et rechercher du contenu basé sur Ann. Il y a maintenant une jointure extérieure complète pour combiner la recherche vectorielle et lexicale. Cela vous permet de trouver du contenu qui peut être manqué par l’une des méthodes. Un score hybride est créé sur la base du score vectoriel et du score lexical. Ce score de pertinence combiné est utilisé pour ré-classez le résultat, plaçant les plus pertinents en haut des résultats de la recherche.

// Build an Optic Query Plan to find the relevant chunks. MarkLogic utilizes TF/IDF by default.
// However, you can configure it to utilize BM25
PlanSearchOptions planSearchOptions = op.searchOptions()
        .withScoreMethod(PlanSearchOptions.ScoreMethod.BM25)
        .withBm25LengthWeight(0.5);

// The following query plan searches for the matching fragments utilizing the key concepts that have been
// extracted from the knowledge graph. It joins in a view that contains the vector embedding for the chunk
// and re-ranks the results utilizing a hybrid score of cosine similarity and the BM25 score.

// Build the lexical search plan 
PlanBuilder.ModifyPlan lexicalSearchPlan =
  op.fromSearchDocs(andQuery, null, planSearchOptions)
    
    .limit(LEXICAL_LIMIT)

// Fetch the vector embedding for the user's query to re-rank the search results
Response<Embedding> response = this.embeddingModel.embed(query.text());

Map<String, Object> options = new HashMap<>();
PlanBuilder.ModifyPlan vectorSearchPlan =
    op.fromView("GenAI", "Chunks", null, op.fragmentIdCol("vectorsDocId"))
        .joinDocUri(op.col("uri"), op.fragmentIdCol("vectorsDocId"))
        .annTopK(
            TOP_K_COUNT,
            op.col("embedding"),
            op.vec.vector(op.xs.floatSeq(response.content().vector())),
            op.col("distance"),
            options)
        .orderBy(op.asc(op.col("distance")));

// Set default values in case a document is found in only one plan.
ServerExpression scoreBinding = op.caseExpr(op.when(op.isDefined(op.col("score")), op.col("score")), op.elseExpr(op.xs.longVal(0L)));
ServerExpression distanceBinding = op.caseExpr(op.when(op.isDefined(op.col("distance")), op.col("distance")), op.elseExpr(op.xs.longVal(2L)));

PlanBuilder.ModifyPlan plan =
    lexicalSearchPlan
        .joinFullOuter(
            vectorSearchPlan,
            op.on(op.fragmentIdCol("fragmentId"), op.fragmentIdCol("vectorsDocId"))
        )
        .bind(op.as("score", scoreBinding))
        .bind(op.as("distance", distanceBinding))
        .bind(op.as("hybridScore", op.vec.vectorScore(op.col("score"), op.col("distance"))))
        .joinInner(
            op.fromView("Article", "Metadata"),
            op.on(op.col("contentURI"), op.col("source"))
        )
        .orderBy(op.desc("hybridScore"))
        .limit(FINAL_LIMIT);

Ce
approche de recherche hybrideVous permet de créer votre flux de récupération pour trouver à la fois des concepts sémantiquement similaires et des correspondances exactes comme les noms et les étiquettes appropriés. Cela fait surface les documents les plus pertinents pour éclairer la sortie du modèle génératif.

KNN (K-Dearest Neighbors **) ** est une méthode exacte qui trouve les points les plus proches d’une requête, mais il peut être plus lent pour les ensembles de données plus grands. KNN exploite des algorithmes tels que la distance euclidienne ou la distance de cosinus pour déterminer le point de placard. Dans certains cas, vous pouvez préférer la précision à l’efficacité.

Nous adopterons une approche similaire à l’exemple ci-dessus. Cependant, notez que dans cet exemple, la recherche lexicale est d’abord exécutée, puis les éléments sont recommandés à l’aide du score vectoriel.


// Build an Optic Query Plan to find the relevant chunks. MarkLogic utilizes TF/IDF by default.
// However, you can configure it to utilize BM25
PlanSearchOptions planSearchOptions = op.searchOptions()
        .withScoreMethod(PlanSearchOptions.ScoreMethod.BM25)
        .withBm25LengthWeight(0.5);

// The following query plan searches for the matching fragments utilizing the key concepts that have been
// extracted from the knowledge graph. It joins in a view that contains the vector embedding for the chunk
// and re-ranks the results utilizing a hybrid score of cosine similarity and the BM25 score.
PlanBuilder.ModifyPlan plan =
        op.fromSearchDocs(andQuery, null, planSearchOptions)
                .joinInner(
                        op.fromView("GenAI", "Chunks", null, op.fragmentIdCol("vectorsDocId")),
                        op.on(op.fragmentIdCol("fragmentId"), op.fragmentIdCol("vectorsDocId"))
                )
                .limit(20)
                .bind(op.as(op.col("distance"), op.vec.cosine(
                        op.vec.vector(op.col("embedding")),
                        op.vec.vector(op.xs.floatSeq(response.content().vector()))
                )))
                .bind(op.as("hybridScore",
                        op.vec.vectorScore(op.col("score"), op.col("distance"))
                ))
                .orderBy(op.desc(op.col("hybridScore")))
                .limit(5)
                .joinInner(
                        op.fromView("Article", "Metadata"),
                        op.on(op.col("contentURI"), op.col("source"))
                );

Dans les systèmes avec des ensembles de données plus grands, c’est une bonne approche pour pré-franchir les éléments que vous comparez. L’approche hybride fonctionne ici bien en filtrant le contenu avec les correspondances lexicales.

Fusion de rang réciproque (RRF) vs score vectoriel de Marklogic

Fusion de rang réciproque ou RRFest une méthode pour combiner les scores de différents algorithmes. Dans notre exemple, nous utilisons BM25 pour déterminer les scores de pertinence pour les recherches lexicales et la distance en cosinus pour calculer la similitude entre les vecteurs. En eux-mêmes, ces valeurs sont incompatibles. RRF prend ces scores et les combine d’une manière qui permet de trier les articles. Les éléments qui apparaissent dans les deux ensembles de résultats apparaîtront plus élevés.

La fonction de score vectorielle du serveur MarkLogic permet un contrôle plus granulaire sur les entrées. Dans l’exemple de code ci-dessus, nous l’étiquetons comme un score «hybride». La formule du score vectoriel améliore le score à base lexicale en incorporant la similitude des vecteurs, augmentant spécifiquement les scores pour des documents avec une similitude de cosinus plus élevée. Cette approche priorise les documents sémantiquement similaires dans les résultats de recherche et est utilisé comme alternative à la méthode RRF pour combiner les résultats de recherche.

Fournir le nouveau contexte au Genai

La dernière partie du workflow RAG sera d’exécuter la requête avec le Genai. Nous allons construire une nouvelle invite qui utilise notre graphique de connaissances sémantiques pour une nouvelle terminologie et les morceaux découverts dans le cadre de la recherche. Cela améliorera le contexte du LLM et fournira une réponse plus précise et traçable.

Les réponses de recherche doivent être converties en une ligne que Langchain4j peut interpréter.

// Convert the row result set to a list of content for langchain4j
RowSet<RowRecord> rows = rowManager.resultRows(plan);

rows.stream().forEach(row -> {
String source = row.getString("source");
String title = row.getString("title");
String abstractText = row.getString("text");
String text = "Title: " + title + " Abstract:" + abstractText;

Metadata metadata = new Metadata();
metadata.put("uri", row.getString("uri"));
metadata.put("title", title);
metadata.put("text", text);
metadata.put("source", source);
metadata.put("score", row.getString("score"));
metadata.put("distance", row.getString("distance"));
metadata.put("hybridScore", row.getString("hybridScore"));

Content content = Content.from(new TextSegment(text, metadata));
chunks.add(content);
});

La requête utilisateur entrante est gérée dans un contrôleur et exploite l’assistant de requête Langchain4j pour ajouter vos connaissances internes dans le contexte du LLM.

public interface QueryAssistant {


    @SystemMessage("You are a researcher. " +
            "1. Answer the question in a few sentences." +
            "2. Answer the question using text provided." +
            "4. Answer the question using chat memory. " +
            "5. If you don't know the answer, just say that you don't know. " +
            "6. You should not use information outside the context or chat memory for answering."
    )
    Result<String> chat(@UserMessage String question);


}



// Build the Query Assistant to interact with the LLM, 
QueryAssistant queryAssistant = AiServices.builder(QueryAssistant.class)
.chatLanguageModel(this.chatLanguageModel)
.chatMemory(this.chatMemory)
.retrievalAugmentor(DefaultRetrievalAugmentor.builder()
.contentRetriever(this.progressDataPlatformQueryRetriever)
.queryTransformer(new CompressingQueryTransformer(chatLanguageModel))
.build()
)
.build();

}

Le résultat final fournit une réponse textuelle à la question ainsi que des liens citationnels pour le contenu utilisé pour répondre à la question.

Image 2: Résultats de la recherche hybride et de l’utilisation des données privées avec le Genai

Dans le prochain blog, nous allons voir comment intégrer ce flux avec le chatbot dans le niveau intermédiaire de votre application React.

Pour explorer le processus complet de conception d’un flux de travail de chiffon afin d’améliorer la précision de vos réponses LLM,
Téléchargez notre livre blanc SEMANT RAG.




Source link
Quitter la version mobile