חיפוש טקסט מלא ב-Postgres עם Hibernate 6

הייברנייט

הייברנייט לבדו אינו מספק תמיכה מלאה בחיפוש מלא על הטקסט. הוא צריך להסתמך על תמיכה של מנוע הבסיס של מסד הנתונים או פתרונות של זולתים.

הרחבה הנקראת חיפוש הייברנייט משתלבת עם אפפכייט לוסין או אלסטיק חיפוש (יש גם אינטגרציה עם OpenSearch).

פוסטגרס

לפוסטגרס הייתה תכונה של חיפוש מלא על הטקסט מגירסה 7.3. למרות שהוא לא יכול להתחרות עם מנועי חיפוש כמו אלסטיק חיפוש או לוסין, הוא עדיין מספק פתרון גמיש וחזק שעשוי להספיק לעמדת המשתמשים של היישומים – תכונות כמו שיפוע, דירוג ואינדקסציה.

נסביר בקצרה כיצד ניתן לבצע חיפוש מלא על הטקסט בפוסטגרס. למידע נוסף, אנא פנה לתיעוד של פוסטגרס. מבחינת ההתאמה הבסיסית של טקסט, החלק החשוב ביותר הוא מפעיל המתמטיקה @@.

זה מחזיר true אם המסמך (אובייקט מסוג tsvector) תואם את השאילתה (אובייקט מסוג tsquery).

הסדר אינו קריטי עבור המפעיל. אז, זה לא משנה אם אנו שמים את המסמך בצד שמאל של המפעיל ואת השאילתה בצד ימין או בסדר אחר.

להדגמה טובה יותר, אנו משתמשים בטבלת מסד הנתונים הנקראת tweet.

SQL

 

create table tweet (
        id bigint not null,
        short_content varchar(255),
        title varchar(255),
        primary key (id)
    )

עם נתונים כאלה:

SQL

 

INSERT INTO tweet (id, title, short_content) VALUES (1, 'Cats', 'Cats rules the world');
INSERT INTO tweet (id, title, short_content) VALUES (2, 'Rats', 'Rats rules in the sewers');
INSERT INTO tweet (id, title, short_content) VALUES (3, 'Rats vs Cats', 'Rats and Cats hates each other');

INSERT INTO tweet (id, title, short_content) VALUES (4, 'Feature', 'This project is design to wrap already existed functions of Postgres');
INSERT INTO tweet (id, title, short_content) VALUES (5, 'Postgres database', 'Postgres is one of the widly used database on the market');
INSERT INTO tweet (id, title, short_content) VALUES (6, 'Database', 'On the market there is a lot of database that have similar features like Oracle');

עכשיו בואו נראה איך מראה אובייקט tsvector עבור עמודת short_content עבור כל אחת מהרשומות.

SQL

 

SELECT id, to_tsvector('english', short_content) FROM tweet;

פלט:

הפלט מראה איך to_tsvector המרת עמודת הטקסט לאובייקט tsvector עבור הפרוטוקט חיפוש טקסטים 'english'.

פרוטוקט חיפוש טקסטים

הפרמטר הראשון של פונקציית to_tsvector שהועברה בדוגמה לעיל היה שם הפרוטוקט חיפוש טקסטים. במקרה זה, זה היה "english". על פי מסמך פוסטגרס, פרוטוקט החיפוש הטקסטי הוא כדלקמן:

… פונקציונליות חיפוש טקסט מלא כוללת את היכולת לעשות דברים רבים יותר: לדלג על חידושיות מסוימות (מילים עצלה), לעבד מונחים שקולים, ולהשתמש בניתוח מתוחכם, למשל, לנתח בהתבסס על יותר מסתם רווחים. פונקציונליות זו נשלטת על ידי פרוטוקטים חיפוש טקסט.

אז, הצרוך הוא חלק קריטי בתהליך וחיוני לתוצאות החיפוש המלאות שלנו. עבור תצרוכים שונים, המנוע פוסטרגר יכול להחזיר תוצאות שונות. זה לא חייב להיות המצב בין מילון של שפות שונות. לדוגמה, אפשר לקבל שני תצרוכים לשפה אחת, אך אחד מתעלם משמות המכילים ספרות (לדוגמה, מספרים סדרתיים מסוימים). אם נשלח את השאילתה שלנו את המספר הסדרתי הספציפי שאנו מחפשים, שהוא חובה, לא נמצא שום רשומה עבור התצרוך שמתעלם ממילים עם מספרים. גם אם יש לנו רשומות כאלה במסד הנתונים, אנא בדוק את תיעוד התצרוך לקבלת מידע נוסף.

שאילתת טקסט

שאילתת טקסט תומכת בכמה פעולות כמו & (AND), | (OR), ! (NOT), ו-<-> (FOLLOWED BY). הפעולות השלושה הראשונים אינן דורשות הסבר מפורט. הפעולה <-> בודקת האם המילים קיימות והאם הן מופיעות בסדר מסוים. לכן, למשל, עבור השאילתה "rat <-> cat", אנו מצפים שהמילה "cat" תתקיים, ואחריה "rat".

דוגמאות

  • תוכן המכיל את rat ואת cat:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'Rat & cat');

  • תוכן המכיל את database ואת market, וה-market הוא המילה השלישית אחרי מסד נתונים:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'database <3> market');

  • תוכן המכיל את database אך לא את Postgres:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'database & !Postgres');

  • תוכן המכיל Postgres או Oracle:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'Postgres | Oracle');

פונקציות ערופים

כבר הוזכרה במאמר זה אחת הפונקציות הערופיות היוצרות שאילתות טקסט, שהיא to_tsquery. יש עוד פונקציות כאלה כמו:

  • plainto_tsquery
  • phraseto_tsquery
  • websearch_to_tsquery

plainto_tsquery

הפונקציה plainto_tsquery ממירה את כל המילים המועברות לשאילתה שבה כל המילים משולבות עם מפעיל ה-& (AND). לדוגמה, המקבילה של plainto_tsquery('english', 'Rat cat') היא to_tsquery('english', 'Rat & cat').

לשימוש הבא:

SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ plainto_tsquery('english', 'Rat cat');

אנו מקבלים את התוצאה הבאה:

phraseto_tsquery

הפונקציה phraseto_tsquery ממירה את כל המילים המועברות לשאילתה שבה כל המילים משולבות עם מפעיל <-> (FOLLOW BY). לדוגמה, המקבילה של phraseto_tsquery('english', 'cat rule') היא to_tsquery('english', 'cat <-> rule').

לשימוש הבא:

SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ phraseto_tsquery('english', 'cat rule');

אנו מקבלים את התוצאה הבאה:

websearch_to_tsquery

הפונקציה websearch_to_tsquery משתמשת בתחביר חלופי ליצירת שאילתא טקסט תקינה.

  • טקסט ללא מרכאות: המרה של חלק מהתחביר באותו אופן כמו plainto_tsquery
  • טקסט מסומן במרכאות: המרה של חלק מהתחביר באותו אופן כמו phraseto_tsquery
  • או: המרה לאופרטור "|" (או)
  • "-": כמו "!" (לא) אופרטור

לדוגמה, המקבילה של הwebsearch_to_tsquery('english', '"cat rule" or database -Postgres') היא to_tsquery('english', 'cat <-> rule | database & !Postgres').

לשימוש הבא:

SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ websearch_to_tsquery('english', '"cat rule" or database -Postgres');

אנו מקבלים את התוצאה הבאה:

תמיכת מקור של Postgres ו-Hibernate

כפי שצוין במאמר, ל-Hibernate לבדה אין תמיכה בחיפוש שלמים. היא צריכה להסתמך על תמיכת מנוע המסד הנתונים. זה אומר שאנו רשאים לבצע שאילתות SQL מקוריות כמופיעות בדוגמאות הבאות:

  • plainto_tsquery
Java

 

public List<Tweet> findBySinglePlainQueryInDescriptionForConfigurationWithNativeSQL(String textQuery, String configuration) {
        return entityManager.createNativeQuery(String.format("select * from tweet t1_0 where to_tsvector('%1$s', t1_0.short_content) @@ plainto_tsquery('%1$s', :textQuery)", configuration), Tweet.class).setParameter("textQuery", textQuery).getResultList();
    }

  • websearch_to_tsquery
Java

 

public List<Tweet> findCorrectTweetsByWebSearchToTSQueryInDescriptionWithNativeSQL(String textQuery, String configuration) {
        return entityManager.createNativeQuery(String.format("select * from tweet t1_0 where to_tsvector('%1$s', t1_0.short_content) @@ websearch_to_tsquery('%1$s', :textQuery)", configuration), Tweet.class).setParameter("textQuery", textQuery).getResultList();
    }

Hibernate עם ספריית posjsonhelper

הספרייה posjsonhelper היא פרויקט פתוח שמעניק תמיכה בשאילתות של Hibernate לפונקציות JSON של PostgreSQL וחיפוש שלמים.

לפרויקט ה-Maven, עלינו להוסיף את ה-תלבושות הבאות:

XML

 

<dependency>
    <groupId>com.github.starnowski.posjsonhelper.text</groupId>
    <artifactId>hibernate6-text</artifactId>
    <version>0.3.0</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.4.0.Final</version>
</dependency>

כדי להשתמש ברכיבים הקיימים בספריית ה-posjsonhelper, עלינו לרשום אותם במסד הנתונים של היג'יניבר.

זה אומר שחייב להיות ממשק org.hibernate.boot.model.FunctionContributor מיושם מסוים. הספריה כוללת מימוש של הממשק הזה, שהוא com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor.

A file with the name "org.hibernate.boot.model.FunctionContributor" under the "resources/META-INF/services" directory is required to use this implementation.

קיימת דרך נוספת לרשום את רכיבי posjsonhelper, שניתן לעשות זאת באמצעות תכנות. כדי לראות איך לעשות זאת, בדוק את ה-קישור הזה.

כעת, ניתן להשתמש במפעילי חיפוש מלא בשאילתות של היג'יניבר.

PlainToTSQueryFunction

זהו רכיב העוטף את הפונקציה plainto_tsquery.

Java

 

public List<Tweet> findBySinglePlainQueryInDescriptionForConfiguration(String textQuery, String configuration) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tweet> query = cb.createQuery(Tweet.class);
        Root<Tweet> root = query.from(Tweet.class);
        query.select(root);
        query.where(new TextOperatorFunction((NodeBuilder) cb, new TSVectorFunction(root.get("shortContent"), configuration, (NodeBuilder) cb), new PlainToTSQueryFunction((NodeBuilder) cb, configuration, textQuery), hibernateContext));
        return entityManager.createQuery(query).getResultList();
    }

עבור הגדרה עם הערך 'english', הקוד יפיק את הצהרת הבאה:

Java

 

select
        t1_0.id,
        t1_0.short_content,
        t1_0.title 
    from
        tweet t1_0 
    where
        to_tsvector('english', t1_0.short_content) @@ plainto_tsquery('english', ?);

PhraseToTSQueryFunction

רכיב זה עוטף את הפונקציה phraseto_tsquery.

Java

 

public List<Tweet> findBySinglePhraseInDescriptionForConfiguration(String textQuery, String configuration) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tweet> query = cb.createQuery(Tweet.class);
        Root<Tweet> root = query.from(Tweet.class);
        query.select(root);
        query.where(new TextOperatorFunction((NodeBuilder) cb, new TSVectorFunction(root.get("shortContent"), configuration, (NodeBuilder) cb), new PhraseToTSQueryFunction((NodeBuilder) cb, configuration, textQuery), hibernateContext));
        return entityManager.createQuery(query).getResultList();
        }

לצורך הגדרה עם הערך 'english', הקוד יוצר את הצהרה הבאה:

SQL

 

select
        t1_0.id,
        t1_0.short_content,
        t1_0.title 
    from
        tweet t1_0 
    where
        to_tsvector('english', t1_0.short_content) @@ phraseto_tsquery('english', ?)

WebsearchToTSQueryFunction

רכיב זה עוטף את הפונקציה websearch_to_tsquery.

Java

 

public List<Tweet> findCorrectTweetsByWebSearchToTSQueryInDescription(String phrase, String configuration) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tweet> query = cb.createQuery(Tweet.class);
        Root<Tweet> root = query.from(Tweet.class);
        query.select(root);
        query.where(new TextOperatorFunction((NodeBuilder) cb, new TSVectorFunction(root.get("shortContent"), configuration, (NodeBuilder) cb), new WebsearchToTSQueryFunction((NodeBuilder) cb, configuration, phrase), hibernateContext));
        return entityManager.createQuery(query).getResultList();
    }

לצורך הגדרה עם הערך 'english', הקוד יוצר את הצהרה הבאה:

SQL

 

select
        t1_0.id,
        t1_0.short_content,
        t1_0.title 
    from
        tweet t1_0 
    where
        to_tsvector('english', t1_0.short_content) @@ websearch_to_tsquery('english', ?)

שאילתות HQL

כל הרכיבים שהוזכרו יכולים לשמש בשאילתות HQL. כדי לבדוק איך זה יכול להיעשות, אנא לחץ על קישור.

למה להשתמש בספריית posjsonhelper כשאפשר להשתמש בגישה המקורית עם Hibernate?

למרות שקיבוצת מחרוזת דינמית שאמורה להיות שאילתת HQL או SQL עשויה להיות קלה, יישום פרדיקטים יהיה פרקטיקה טובה יותר, במיוחד כשעליך לטפל בקריטריונים לחיפוש המבוססים על תכונות דינמיות מה-API שלך.

מסקנה

כפי שצוין במאמר הקודם, תמיכת חיפוש מלא-טקסט ב-Postgres יכולה להוות אלטרנטיבה טובה למנועי חיפוש משמעותיים כמו Elasticsearch או Lucene, במקרים מסוימים. זה יכול לחסוך לנו את ההחלטה להוסיף פתרונות של שלישיות לעמדת הטכנולוגיה שלנו, שיכולה גם להוסיף מורכבות נוספת ועלויות נוספות.

Source:
https://dzone.com/articles/postgres-full-text-search-with-hibernate-6