ApexでSOQLとSOSLとミックスさせて検索体験を向上させる

# はじめに

今回はApexで取引先名のレコード検索の検索体験を向上させるために
`SOQL`と`SOSL`をミックスさせて検索してみようと思います。

# SOSLとSOQLの違い

https://help.salesforce.com/s/articleView?id=000385852&type=1

SOQLとSOSLは検索時の優先事項、1クエリで検索するオブジェトの数、Salesforce内の機能で使い分けが主な違いです。

||SOQL|	SOSL|
|---|---|---|
|正式名称|	Salesforce Object Query Language|	Salesforce Object Search Language|
|使用場所|	リストビュー、レポート、Apex	|(グローバル、サイドバー、詳細) 検索、Apex|
|インデックス付けの実行|	同期的 (カスタムインデックスまたは標準インデックス)	|非同期的に実行。通常 2 ～ 3 分。9000 を超えるレコードを一度に読み込んだ場合、超過分は一括インデックスキューに移動される  (低速)。|
|検索時の優先事項|	正確性。条件に一致するすべての結果を返す。 	|関連性と速度。Google 検索に類似。最近参照されたレコードを優先。 
|検索範囲|	一度に 1 オブジェクトを検索可能。|	一度に複数のオブジェクトを検索可能。 |
>引用:https://help.salesforce.com/s/articleView?id=000385852&type=1

# なぜ、SOQLとSOSLを混ぜるの？

SOSLはグローバル検索に使われてるものなので基本は意図したものが返ってきますが
検索の特徴として文字列を単語区切りで認識しているので検索クエリによっては漏れてしまうことがあります。

Salesforce内での検索機能で意図したものが取れないと誤って重複レコードを作成してしまう要因にもなってしまいます。

サンプルとして3つのレコードを用意してみました。

`検索クエリAB`だとSOQLとSOSLでどのレコードが取得できるでしょうか？

![スクリーンショット 2023-12-24 9.45.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/445600/0a32c7e3-6ec4-ee45-fdf5-b626f4c067ab.png)




以下の結果になりました。

|SOQL|SOQL|
|---|---|
|AABB、AAB、AABCC|検索結果なし|

これは極端の例ですが、連続している言葉は1語として捉えるという仕様によるものです。

以下の記がとてもわかりやすく挙動をまとめてくれています。

https://www.n-sysdes.co.jp/blog/18732/

# SOQLとSOSLを併用するApex

```apex: SearchAccount.cls
public with sharing class SearchAccount {
    public SearchAccount(String searchKeyword, Integer limitNum) {
      // SOQLで検索
      List<Account> accountsBySoql = getAccountByNameSOQL(searchKeyword, limitNum);
      System.debug('----SOQL----');
      System.debug(accountsBySoql);
      System.debug(accountsBySoql.size());
      // SOSLで検索
      List<Account> accountsBySosl = getAccountByNameSOSL(searchKeyword, limitNum);
      System.debug('----SOSL----');
      System.debug(accountsBySosl);
      System.debug(accountsBySosl.size());
      // SOSL SOSLの検索結果をマージ
      Set<Account> accountSets = new Set<Account>();
      accountSets.addAll(accountsBySoql);
      accountSets.addAll(accountsBySosl);
      List<Account> deduplicationAccounts = new List<Account>(accountSets);

      System.debug('----検索結果----');
      System.debug(deduplicationAccounts);
    }

    private static List<Account> getAccountByNameSOQL(
      String searchKeyword,
      Integer limitNum
    ) {
      searchKeyword = '%' + searchKeyword + '%';
      List<Account> accounts = [
        SELECT Id, Name
        FROM Account
        WHERE
          Name LIKE :searchKeyword
        LIMIT :limitNum
      ];
  
      return accounts;
    }

    private static List<SObject> getAccountByNameSOSL(
      String searchKeyword,
      Integer limitNum
    ) {
      List<List<SObject>> searchSosl = [
        FIND :searchKeyword
        IN NAME FIELDS
        RETURNING
          Account(
            Id,
            Name
          LIMIT :limitNum)
      ];
      if (searchSosl.isEmpty()) {
        return null;
      }
  
      return searchSosl[0];
    }
}
```
## 結果のマージについて

- SOQLとSOSLの検索結果をそれぞれ Listにする
- Setに検索結果を入れて重複排除
- SetをListに入れ直す

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_set.htm

という作業で検索結果をマージしつつ、Listに変換しています。

```apex
// SOQLで検索
List<Account> accountsBySoql = getAccountByNameSOQL(searchKeyword, limitNum);
// SOSLで検索
List<Account> accountsBySosl = getAccountByNameSOSL(searchKeyword, limitNum);
// SOSL SOSLの検索結果をマージ
Set<Account> accountSets = new Set<Account>();
accountSets.addAll(accountsBySoql);
accountSets.addAll(accountsBySosl);
List<Account> deduplicationAccounts = new List<Account>(accountSets);
```

## 実行

開発者コンソールもしくは、SalesforceCLIで実行します。

```apex
SearchAccount searchAccount = new SearchAccount('AB', 100);
```

## 実行結果

SOSLで検索しきれなかったものはSOQLの曖昧検索によって取得できています。

```diff
09:45:41.49 (63328503)|USER_DEBUG|[5]|DEBUG|----SOQL----
+09:45:41.49 (63465997)|USER_DEBUG|[6]|DEBUG|(Account:{Id=0011Q00002aN3ygQAC, Name=AABB}, Account:{Id=0011Q00002aN3ylQAC, Name=AAB}, Account:{Id=0011Q00002aN3ybQAC, Name=AABCC})
09:45:41.49 (63576355)|USER_DEBUG|[7]|DEBUG|3
09:45:41.49 (160393968)|USER_DEBUG|[10]|DEBUG|----SOSL----
+09:45:41.49 (160447999)|USER_DEBUG|[11]|DEBUG|()
09:45:41.49 (160483678)|USER_DEBUG|[12]|DEBUG|0
09:45:41.49 (162147839)|USER_DEBUG|[19]|DEBUG|----検索結果----
+09:45:41.49 (162288799)|USER_DEBUG|[20]|DEBUG|(Account:{Id=0011Q00002aN3ygQAC, Name=AABB}, Account:{Id=0011Q00002aN3ylQAC, Name=AAB}, Account:{Id=0011Q00002aN3ybQAC, Name=AABCC})
```

# どういう時にこのApexを使う？

取引先名を検索する
画面フローの検索ロジックとして使うといいと思います。

今回はSOSLでAccoutのみですが、Leadを加えればかなり利用者の検索したいレコードを取得できる確率が高まるのではないでしょうか

`InvocableMethod`アノテーションを付与すればフローのアクションとして利用できるようになります。

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation_InvocableMethod.htm

# 最後に

今回は自前でSOQLとSOSLを合わせましたが、
契約書とかの非構造データも検索できるようにすると面白そうです。
Salesforceが将来的にLLMの結果とかをグローバル検索に合わせて検索できるようにしてくれるかもしれませんね。