データベース (RDB) と比べた際の Algolia の気になった点

# データベース (RDB) と比べた際の Algolia の気になった点

検索機能を提供する [**Algolia**](https://www.algolia.com/) について
データベースの機能と比較した際に気になった点を書いてみます。

当記事の内容は [**公式のドキュメント**](https://www.algolia.com/doc/) に書いてあることなので
正確な情報はそちらを確認するようしてください。

[**Algolia の使用感がわかる Live Demo (InstantSearch)**](https://codesandbox.io/s/github/algolia/doc-code-samples/tree/master/InstantSearch.js/getting-started)

## `Index` には環境識別子をつけましょう

[**All Environments on the Same Application**](https://www.algolia.com/doc/guides/sending-and-managing-data/manage-environments/#all-environments-on-the-same-application)

RDB でのテーブルに近い位置となる `Index` ですが、
同一の `application` に複数の環境のものを用意したほうがよいとされています。

`products` とすると `dev_products`, `staging_products`, `prod_products` などですね。

> Unless you are on an Enterprise plan and have a dedicated cluster of servers, having multiple applications generate an invoice per application.

これは実装上での必要性というか **「application ごとに環境を分けてもいいけどそれぞれに請求が行くよ」** という記述によるところです。

## 演算子について

### 等号

RDB の SQL の `WHERE` に該当するといえる Algolia の [**Filtering**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/) にて使用する `=` などの等号に差が・・・

```sql:sql-equality-operator
-- 文字列
column_1 = '対象文字列'
-- 数字
column_2 = 123
```

これが Algolia の場合には以下のようになります。

#### 文字列の場合 `:` を使用

[**Filter By String**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-string/)

Algolia の通常の検索 (入力などに対して近しいレコードを上位に位置させる) でなく、
SQL の `WHERE` のように一致しない候補を結果から外すには
**文字列の場合に** `attributesForFaceting` の設定が必要です。

[**Configuring attributesForFaceting**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-string/#configuring-attributesforfaceting)

> 
```js:configuring-attributesforfaceting
index.setSettings({
  attributesForFaceting: [
    'brand' // or 'filterOnly(brand)' for filtering purposes only
  ]
})
```

これで `brand` に対して Filtering が可能となり、以下で `Motorola` のものを抽出します。
[**Applying a string filter**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-string/#applying-a-string-filter)

> 
```js:algolia-string-equality-operator
// Only “Motorola” smartphones
index.search('smartphone', {
  filters: 'brand:Motorola'
})
```

#### `数字` `boolean` の場合 `=` を使用

[**Filter By Numeric Value**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-numeric-value/)
[**Filter By Boolean**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-boolean/)

Algolia では数字 (とみなされる値) については `=` を使用します。
また、文字列において必要となっていた `attributesForFaceting` の設定は不要です。
[**1. Define attributes that need to be filterable (at indexing time)**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/#define-attributes-that-need-to-be-filterable-at-indexing-time)
> Note that if you need to filter on numeric or boolean attributes, you don’t need to set them as attributesForFaceting.

以下にてそれぞれの抽出を行っています。

- `price` が `100` のもの
- `is_available` が `true` のもの
    - `true` は `1`
    - `false` は `0`

> 
```js:algolia-numeric-value-equality-operator
index.search('query', {
  filters: `price = 100`
})
・・・
index.search('query', {
  filters: `is_available=1`
})
```

##### BETWEEN

[**Filtering by price range**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-numeric-value/#filtering-by-price-range)

```sql:sql-between
price BETWEEN 100 AND 200
```


数字でも `BETWEEN` のような記述については `:` も使用するようです。

```js:algolia-between
index.search('query', {
  filters: 'price:100 TO 200'
})
```

### 値の型のバリエーションを考慮した関数

前述の通り、文字列と数値で等号にバリエーションがあるため、
それを吸収するための関数を用意しておくとよいかもしれません。

```js:algolia-euqal
/**
 * 文字列、数字の等号の指定を吸収する関数
 * @param {string} attr 評価対象の `Attribute`
 * @param {string | number} val 評価対象の `Attribute` の値
 * @return {string} `attr=val` or `attr:val`
 */
const equal = (attr, val) => {
  const operator = typeof val === 'string' ? ':' : '='
  return [attr, operator, val].join('')
}
```

### 配列への検索

Algolia は `Index` の `Record` に配列も格納することができるので、それに対する `Filtering` も可能です。

[**Filter an Array**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-arrays/)

配列の要素が **文字列であれば [attributesForFaceting](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-arrays/#configuring-attributesforfaceting-at-indexing-time) の設定が必要** です。

以下では `categories` に `politics` が含まれているものを抽出します。

```js:algolia-array-includes
index.search('harry', {
  filters: 'categories:politics'
})
```

逆に **`politics` が含まれていないもの** が欲しい場合 `NOT` による否定で実現が可能です。

```js:algolia-array-not-includes
index.search('harry', {
  filters: 'NOT categories:politics'
})
```

要素が数字である場合 `attributesForFaceting` が不要で、演算子は `=` を使用します。

```js:algolia-array-includes-numeric
index.search('harry', {
  filters: 'types = 123'
})
```

### `AND` `OR` `NOT` について

内容については [**Filters and Boolean Operators**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators/) のピックアップなので詳細はリンクを確認してください。

また、記述が正しいかの確認が [**Filters syntax validator**](https://www.algolia.com/doc/api-reference/api-parameters/filters/#filters-syntax-validator) により可能です。

`AND`, `OR` それぞれの単体については SQL の使用と変わりません。
条件の `論理積` `論理和` が求められます。

> The following operators, which must be specified in CAPS

ただし使用する際には ~~`and`~~ や ~~`Or`~~ など小文字を含まない、すべて大文字である必要があるようです。

以下においては `AND` により条件が結合されています。

- `category` が `Book` か `Ebook` であり (`OR`)
- `author` が `JK Rowling` でないもの (`NOT`)

> 
```js:algolia-operator-and-or
index.search('', {
  filters: '(category:Book OR category:Ebook) AND NOT author:"JK Rowling"'
})
```

```sql:sql-operator-and-or
(category = 'Book' OR category = 'Ebook')
AND
NOT author = 'JK Rowling'
```

#### [AND OR に対して Algolia での注意事項](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators/#caveats)

##### `(A AND B)` など括弧でグループ化した条件は `NOT` で否定できない

[**Negating Combined Filters**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators/#negating-combined-filters)

> You cannot negate a group of filters, only an individual filter. For example, NOT(filter1 OR filter2) is not allowed.

`()` によりグループ化した複合条件に対して `NOT` により否定することができません。

```js:algolia-cannot-negate-a-group-of-filters
index.search('', {
  // これはダメ
  filters: 'NOT (author:"JK Rowling" AND author:"JRR Tolkien")'
})
```

##### 括弧による条件のグループ化に制約あり

[**Combining ANDs and ORs**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators/#combining-ands-and-ors)

> For performance reasons, we do not allow you to put groups of ANDs within ORs.

`AND` でグループ化した結果を `OR` でさらに再グループ化をすることを許可していないとのことです。

> We allow ( x AND (y OR z) AND (a OR b) )

`OR` をグループ化したものを `AND` で再グループ化なので **OK**

> We allow ( x AND y OR z AND a OR b )

グループ化してないので **OK**

> We don’t allow ~~( x OR ( y AND z) OR ( a AND b) )~~

`AND` でグループ化された `( y AND z)`, `( a AND b)` を含んで `OR` により結合しているため **NG**

> We don’t allow ~~(a AND b) OR (c AND d)~~

`AND` でグループ化した条件のみで `OR` による再グループ化を行っているため **NG**

##### 括弧は補完される

[**Missing parentheses**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators/#missing-parentheses)

以下のような `AND` と `OR` が混ざっているケースでは `OR` の部分で勝手にグループ化されるようです。

> 
( x AND y OR z AND a OR b ) becomes ( x AND (y OR z) AND (a OR b) )
( x OR y AND z OR a AND b ) becomes ( (x OR y) AND (z OR a) AND b )

> Given the potential for confusion, we encourage you to use parenthesis.

「Algolia 側で括弧は付加されるので混乱しないよう括弧はつけるようにしたほうがいいですよ」

##### 複数の型を `OR` で結合は不可

[**Mixing type of filters in OR conditions**](https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/in-depth/combining-boolean-operators/#mixing-type-of-filters-in-or-conditions)

`OR` により型の違う条件をグループ化することはだめとのことです。

> For example, `num=3 OR tag1 OR facet:value` is not allowed.

数字の `num=3`, Boolean の `tag1`, 文字列の `facet:value` となりだめなようです。

## `Records` の格納先はテーブルではなく `Index`

[**The Index**](https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/#the-index)
RDB ではテーブルにレコードを格納しますが、 Algolia での格納先は `Index` となります。
`JOIN` とかはなく `Index` の `Record` に必要に応じて非正規化を行って突っ込むようです。

## `ORDER BY`

**そんなものウチにはないよ・・・**

Algolia では問い合わせ時に **`Index` への並べ替え** を要求できません。
しかし並べ替え自体は当然可能です。

[**Sorting Results**](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/)

ただし並べ替えの前提として `Replica` の概念を知る必要があります。

[**Understanding Replicas**](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/)

`Replica` についてはざっくりと以下の通りです。

- `Index` をコピーしたものが `Replica`
- `Index` のデータである `Records` の `登録` ・ `更新` ・ `削除` は `Replica` に同期される
- `Replica` は **並べ替えなどの独自の設定を持つことができる**

つまり・・・
SQL では `ORDER BY` により並べ替えを行うことができますが

```sql:sql-order-by
ORDER BY
  price ASC -- or `price DESC` で降順へ
```

Algolia では以下のような対応が必要となります。

1. `price` の降順の並べ替えを行いたい場合、 `Replica` を作成し並べ替え設定に `price` を設定
    - 元の `Index` が `products` であれば `Replica` として `products_price_desc` を作成
    - 並べ替え設定に `price` の降順となる設定を追加

2. 検索を行う際に **検索対象を products から products_price_desc へ切り替えを行う**