So funktioniert der Post-Filter von Solr

In diesem Beitrag gebe ich Ihnen ein konkretes Beispiel für die benutzerdefinierte Filterung von Beiträgen für den Fall, dass Sie Dokumente auf der Grundlage von Zugriffskontrolllisten filtern.

13. Mai 2015: Es wurde ein Code-Update für Solr 5.x durchgeführt – Alle Details in einem neuen Blog-Beitrag.

Dez. 6, 2012: Es wurde ein Code-Update für Solr 4.0 vorgenommen (siehe kommentierter Abschnitt in AccessControlQParserPlugin.java unten)

Yonik hat vor kurzem über „Advanced Filter Caching in Solr“ geschrieben, in dem er über teure und benutzerdefinierte Filter sprach; die Details der Implementierung überließ er dem Leser. In diesem Beitrag werde ich ein konkretes Beispiel für benutzerdefinierte Post-Filterung für den Fall liefern, dass Dokumente auf der Grundlage von Zugriffskontrolllisten gefiltert werden.

Zusammenfassung der Filterung und des Caching von Solr

Lassen Sie uns zunächst die Filter- und Caching-Funktionen von Solr betrachten. Abfragen an Solr beinhalten eine Volltextabfrage mit Relevanzbewertung(der berüchtigte q-Parameter). Während der Benutzer navigiert, durchsucht er die Facetten. Die Suchanwendung generiert Filterabfrageparameter (fq) für die facettierte Navigation (z.B. fq=color:red, wie in dem oben genannten Artikel). Die Filterabfragen sind nicht an der Bewertung der Dokumente beteiligt, sondern dienen nur dazu, den Suchraum zu reduzieren. Solr verfügt über einen Filter-Cache, in dem die Dokumentensätze jeder einzelnen Filterabfrage zwischengespeichert werden. Diese Dokumentensätze werden im Voraus generiert, zwischengespeichert und reduzieren die von der Hauptabfrage berücksichtigten Dokumente. Wenn die Filter nicht zwischengespeichert werden, werden sie parallel zur Hauptabfrage verwendet, um die zu berücksichtigenden Dokumente zu „überspringen“. Jedem Filter können Kosten zugeordnet werden, um das Überspringen zu priorisieren (die kleinste Menge zuerst würde die Anzahl der für den Abgleich berücksichtigten Dokumente minimieren).

Filtern von Beiträgen

Auch ohne Caching werden Filtersätze standardmäßig im Voraus generiert. In einigen Fällen kann die Erstellung eines Filtersatzes extrem teuer und unerschwinglich sein. Ein Beispiel hierfür ist die Filterung der Zugriffskontrolle, bei der der Kontext der Benutzerabfrage berücksichtigt werden muss, um zu wissen, welche Dokumente zurückgegeben werden dürfen und welche nicht. Idealerweise sollten nur übereinstimmende Dokumente, d.h. Dokumente, die der Abfrage und den einfachen Filtern entsprechen, für die Sicherheitszugriffskontrolle ausgewertet werden. Es wäre Verschwendung, andere Dokumente auszuwerten, die ohnehin nicht übereinstimmen würden. Lassen Sie uns also ein Beispiel durchspielen… ein erfundenes Beispiel, um zu zeigen, wie die Post-Filterung von Solr funktioniert. Hier ist das Design:

  • Dokumente sind mit einer „Zugriffskontrollliste“ verknüpft, in der erlaubte und nicht erlaubte Benutzer sowie erlaubte und nicht erlaubte Gruppen angegeben sind.
  • Die Zugriffskontrollliste ist eine geordnete Liste von erlaubten/verbotenen Benutzern und Gruppen. Die Reihenfolge ist wichtig, so dass die erste übereinstimmende Regel den Zugriff bestimmt.
  • Wenn kein erlaubter Zugriff gefunden wird, ist das Dokument nicht erlaubt.

Ein Dokument könnte zum Beispiel die Zugriffskontrolle „+u:user1 +g:group1 -g:group2 +u:user2 -u:user3“ enthalten. Abfrageanfragen an Solr enthalten den Benutzernamen und die Gruppenzugehörigkeit des Benutzers. Anhand dieser Beispiel-Zugriffskontrollzeichenkette sehen Sie, wie dieses ausgeklügelte Design reagieren sollte:

user='user1', groups=null: allowed
user='user2', groups=null: allowed
user='user1', groups=[group1]: allowed
user='user2', groups=[group2]: NOT ALLOWED
user='user3', groups=[group1]: allowed
user='user3', groups=[group2]: NOT ALLOWED
user='user3', groups=[group1, group2]: allowed

Das heißt, wenn Benutzer2 als Mitglied von Gruppe2 sucht, sollte er dieses bestimmte Dokument nicht finden dürfen (-g:Gruppe2 steht in den Regeln vor +u:Benutzer2, und die Reihenfolge ist wichtig). Ich weiß, ich weiß, das ist ziemlich konstruiert, aber angesichts einiger Kundenprojekte, die wir kürzlich durchgeführt haben, nicht völlig unrealistisch.

Da diese Regeln von der Reihenfolge und der Abfrage abhängig sind, ist es nicht möglich, eine einfache Lucene-Abfrage durchzuführen, um erlaubte Dokumente zu filtern. Folgen Sie mir bei diesem Beispiel. Ich habe versucht, es ausreichend kompliziert zu gestalten, um diesen Punkt zu verdeutlichen. Solr verfügt über eine relativ neue PostFilter-Funktion, die diese letzte Überprüfung der Filterung von Dokumenten im laufenden Betrieb ermöglicht. Es erfordert einiges an Know-how, um einen PostFilter angemessen zu implementieren, so dass das Codebeispiel hier ein guter Ausgangspunkt für Ihre eigene benutzerdefinierte Post-Filterung ist. Die Nutzung eines PostFilters erfolgt über ein Solr QParserPlugin. Hier ist mein benutzerdefiniertes AccessControlQParserPlugin:

public class AccessControlQParserPlugin extends QParserPlugin {
  public static String NAME = "acl";

  public void init(NamedList args) {
  }

  @Override
  public QParser createParser(String qstr, SolrParams localParams,
                              SolrParams params, SolrQueryRequest req) {
    return new QParser(qstr, localParams, params, req) {

      @Override
      public Query parse() throws ParseException {
        return new AccessControlQuery(localParams.get("user"), localParams.get("groups"));
      }
    };
  }
}

Und dann wird dies wie folgt in solrconfig.xml verdrahtet:

<queryParser name="acl" class="AccessControlQParserPlugin"/>

All das ist nur der notwendige Kleber, um eine PostFilter-Implementierung einzuhängen. Hier ist meine Beispiel-Implementierung:

/**
 * Note that this Query implementation can _only_ be used as an fq, not as a q (it would need to implement createWeight).
 */
class AccessControlQuery extends ExtendedQueryBase implements PostFilter {

  private String user;
  private String[] groups;

  public AccessControlQuery(String user, String groups) {
    this.user = user;
    this.groups = groups.split(",");
  }

  public static boolean isAllowed(String acl, String user, String[] groups) {
    // acl is in the form of a series of whitespace separated [+|-][u|g]:name
    // allowed is determined by any explicit user or group mentions, plus or minus
    // order matters
    // if nothing matches, it is not allowed

    if (user == null && groups == null) return false;

    String[] permissions = acl.split(" ");

    for(String p : permissions) {
      boolean allowed = p.charAt(0) == '+';
      String name = p.substring(3);
      if (p.charAt(1) == 'u') { // user
        if (user != null && user.equals(name)) return allowed;
      } else { // group
        if (groups != null) {
          for (String g : groups) {
             if (g.equals(name)) return allowed;
          }
        }
      }
    }

    return false;
  }

  @Override
  public boolean getCache() {
    return false;  // never cache
  }

  @Override
  public int getCost() {
    return Math.max(super.getCost(), 100);  // never return less than 100 since we only support post filtering
  }

  public DelegatingCollector getFilterCollector(IndexSearcher searcher) {
    return new DelegatingCollector() {
      String[] acls;

      @Override
      public void collect(int doc) throws IOException {
        if (isAllowed(acls[doc], user, groups)) super.collect(doc);
      }

      @Override
      public void setNextReader(IndexReader reader, int docBase) throws IOException {
        acls = FieldCache.DEFAULT.getStrings(reader, "acl");  
        super.setNextReader(reader, docBase);
      }
    };
  }

  // For Solr 4.0, replace getFilterCollector with this one, adjusting
  //public DelegatingCollector getFilterCollector(IndexSearcher searcher) {
  //  return new DelegatingCollector() {
  //    FieldCache.DocTerms acls;

  //    @Override
  //    public void collect(int doc) throws IOException {
  //      final BytesRef br = new BytesRef();
  //      if (isAllowed(acls.getTerm(doc, br).utf8ToString(), user, groups)) super.collect(doc);
  //    }
  //
  //    @Override
  //    public void setNextReader(AtomicReaderContext context) throws IOException {
  //      acls = FieldCache.DEFAULT.getTerms(context.reader(), "acl");   // may be better to use the StringIndex version
  //      super.setNextReader(context);
  //    }
  //
  //
  //  };
  //}

  // NOTE: it is very important to implement proper equals and hashCode methods for this class, as it is used with
  // *result* caching (not filter caching, which is explicitly disabled here).

  @Override
  public String toString() {
    return "AccessControlQuery{" +
        "user='" + user + ''' +
        ", groups=" + (groups == null ? null : Arrays.asList(groups)) +
        '}';
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;

    AccessControlQuery that = (AccessControlQuery) o;

    if (!Arrays.equals(groups, that.groups)) return false;
    if (user != null ? !user.equals(that.user) : that.user != null) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (user != null ? user.hashCode() : 0);
    result = 31 * result + (groups != null ? Arrays.hashCode(groups) : 0);
    return result;
  }

  public static void main(String[] args) {
    String acl = "+u:user1 +g:group1 -g:group2 +u:user2 -u:user3";

    System.out.println("acl = " + acl);

    test(acl, "user1", null);
    test(acl, "user2", null);
    test(acl, "user1", new String[] {"group1"});
    test(acl, "user2", new String[] {"group2"});
    test(acl, "user3", new String[] {"group1"});
    test(acl, "user3", new String[] {"group2"});
    test(acl, "user3", new String[] {"group1","group2"});
  }

  private static void test(String acl, String user, String[] groups) {
    System.out.println("user='" + user + ''' +
        ", groups=" + (groups == null ? null : Arrays.asList(groups)) +
        ": " + (isAllowed(acl, user, groups) ? "allowed" : "NOT ALLOWED"));
  }
}

Die Methode main() wurde verwendet, um die obigen Ergebnisse der Regelverarbeitung zu erzeugen. Ein paar Anmerkungen zu diesem Code sind wichtig:

  • Diese Implementierung kann nur als Parameter für eine Filterabfrage (fq) verwendet werden, nicht als q-Parameter.
  • hashCode/equals sind sehr wichtig, da es sonst zu unerwarteten/falschen Ergebnissen kommen kann.
  • Die Zwischenspeicherung ist explizit deaktiviert, so dass Sie cache=false nicht einstellen müssen.
  • Solr verfügt über eine Logik, die PostFilter nur dann einsetzt, wenn die Kosten >= 100 sind. Deshalb ist die Methode getCost so, wie sie ist.
  • Die benutzerdefinierte Filterlogik befindet sich in der einzigen isAllowed()-Methode.
  • Dieses Beispiel wurde mit der Lucene/Solr 3.x Codebasis erstellt. Es sind einige leichte Anpassungen erforderlich, um es für die 4.x-Codebasis zu optimieren.

In dieser Implementierung werden die Zugriffskontrollregeln für jedes Dokument vollständig im Feld acl angegeben. Um zur Abfragezeit effizient nach diesen Regeln zu filtern, wird der FieldCache von Lucene verwendet. Der Aufbau der FieldCache-Datenstruktur kostet Zeit und Arbeitsspeicher, so dass der Zugriff zur Abfragezeit schnell erfolgen kann. Wenn FieldCache verwendet wird (Sortierung, einige Facettierungsimplementierungen, Funktionsabfragen und dieser benutzerdefinierte Abfrageparser), ist es ratsam, entsprechende Erwärmungsabfragen einzubauen, damit die FieldCache-Einträge zur Festschreibungszeit aufgebaut werden und die Endbenutzer nicht länger zur Abfragezeit warten müssen.

Nachdem wir die Implementierung hinter uns haben, können wir sie nun endlich verwenden: Indizieren Sie einige Dokumente und erstellen Sie Abfragen, die mithilfe des „acl“-Abfrageparsers gefiltert werden. Hier sind die Dokumente, im CSV-Format:

id,acl
1,+u:bob
2,-g:sales +g:engineering
3,+g:hr -g:engineering
4,-u:alice +g:hr
5,+g:hr -u:alice
6,+g:sales +g:engineering -u:bob
7,+g:hr -u:alice +g:sales
8,+g:sales
9,+g:engineering
10,+g:hr

Dies wurde mit dem Solr-Beispiel post.jar indiziert:

java -Dtype=text/csv -Durl=http://localhost:8983/solr/update/csv -jar post.jar example_docs.csv

wobei das acl-Feld als <field name=“acl“ type=“string“ indexed=“true“ stored=“true“ multiValued=“false“/> definiert ist.

Um die Präsentation zu vereinfachen, wurde eine schnelle und schmutzige Velocity-Vorlage, ids.vm, zum Verzeichnis conf/velocity hinzugefügt:

Matching ids:
#if($page.results_found > 0)
  #foreach($doc in $response.results)
    $doc.id
  #end
#else
  None
#end

Und schließlich sehen wir uns die Ergebnisse an, indem wir die Basisabfrage http://localhost:8983/solr/select?q=*:*&wt=velocity&v.template=ids verwenden, die für sich genommen „Übereinstimmende IDs: 1 2 3 4 5 6 7 8 9 10“ ergibt. Durch Anhängen eines fq-Parameters mit der Syntax &fq={!acl user=’username‘ groups=’group1,group2′} wird der Sicherheitsfilter angewendet. Hier sehen Sie verschiedene Variationen von Benutzern und Gruppen und die Ergebnisse:

&fq={!acl user=‚alice‚ groups=“}: Übereinstimmende IDs: Keine

&fq={!acl user=‚bob‚ groups=“}: Übereinstimmende IDs: 1

&fq={!acl user=‚alice‚ groups=‚hr‚}: Übereinstimmende IDs: 3 5 7 10

&fq={!acl user=‚alice‚ groups=‚hr,sales‚}: Übereinstimmende IDs: 3 5 6 7 8 10

&fq={!acl user=‚alice‚ groups=‚hr,sales,engineering‚}: Übereinstimmende IDs: 3 5 6 7 8 9 10

&fq={!acl user=‚bob‚ groups=‚hr‚}: Übereinstimmende IDs: 1 3 4 5 7 10

Kleingedrucktes

Es ist wichtig zu beachten, dass PostFilter ein letzter Ausweg für die Implementierung der Dokumentenfilterung ist. Machen Sie die Lösung nicht komplizierter, als sie sein muss. In den meisten Fällen kann sogar die Filterung der Zugriffskontrolle mit einfachen Suchtechniken implementiert werden, indem erlaubte Benutzer und Gruppen auf Dokumente indiziert werden und der Lucene (oder ein anderer) Query Parser verwendet wird, um die Aufgabe zu erfüllen. Nur wenn die Regeln zu kompliziert sind oder externe Informationen benötigt werden, ist ein benutzerdefinierter PostFilter sinnvoll. Die interne Methode #collect() wird für jedes übereinstimmende Dokument aufgerufen. In diesem Beispiel wurde eine *:*-Abfrage verwendet, die dazu führt, dass jedes Dokument im Index mit dem PostFilter ausgewertet werden muss. Was in #collect geschieht, muss hochgradig optimiert sein. Holen Sie NICHT, ich wiederhole, holen Sie NICHT das Dokument aus dem Lucene-Index in dieser Methode (ich werde nicht einmal erwähnen, welche Methoden Sie meiden sollten)! Wenn Sie auf Felddaten zugreifen müssen, sollten Sie sie in indizierten Feldern mit Einzelwerten haben und den FieldCache verwenden (und eventuell wird die 4.x doc-values Funktion auch hier nützlich sein). Und wenn Sie FieldCache verwenden, fügen Sie eine repräsentative Abfrage mit Ihrem PostFilter zu Ihren Erwärmungsabfragen in solrconfig.xml hinzu.

You Might Also Like

Vom Suchunternehmen zum praktischen KI-Pionier: Unsere Vision für 2025 und darüber hinaus

CEO Mike Sinoway gibt Einblicke in die Zukunft der KI und stellt...

Read More

Wenn KI schief geht: Fehlschläge in der realen Welt und wie man sie vermeidet

Lassen Sie nicht zu, dass Ihr KI-Chatbot einen 50.000 Dollar teuren Tahoe...

Read More

Lucidworks Kernpakete: Branchenoptimierte KI-Such- und Personalisierungslösungen

Entdecken Sie unsere umfassenden Core Packages, die Analytics Studio, Commerce Studio und...

Read More

Quick Links