Flask Mega-Tutorial、パートXVI:全文検索

(2018年版)


ミゲル・グリンバーグ




ここに 戻る


これは、Flask Mega-Tutorialsシリーズの第16部で、全文検索をマイクロブログに追加します。


ネタバレの下には、この2018年シリーズのすべての記事のリストがあります。



注1:このコースの古いバージョンをお探しの場合は、こちらをご覧ください


注2:私(ミゲル)の仕事を支持して突然声をかけたい場合、または1週間記事を待つ忍耐がない場合、私(ミゲルグリーンバーグ)はこのガイドの完全版(英語)を電子書籍またはビデオの形式で提供します。 詳細については、 learn.miguelgrinberg.comをご覧ください


この章の目標は、ユーザーが使い慣れた言語を使用して興味深い投稿を見つけることができるように、マイクロブログの検索機能を実装することです。 多くの種類のWebサイトでは、単にGoogle、Bingなどを有効にすることができます。すべてのコンテンツのインデックスを作成し、検索APIを通じて検索結果を提供します。 これは、フォーラムなど、ほとんど静的なページがあるサイトで機能します。 しかし、私のアプリケーションでは、コンテンツの主要な単位はユーザー投稿であり、これはWebページ全体のごく一部です。 必要な検索は、ページ全体ではなく、これらの個々のブログ投稿を指します。 たとえば、「犬」という単語を検索する場合、さまざまなユーザーのブログにその単語を含む投稿を表示します。 問題は、「犬」という単語(またはその他の検索キーワード)を含むすべての投稿を表示するページが、大規模な検索エンジンが検索およびインデックス付けできるブログページとして存在しないため、他に選択肢がないことです。独自の検索機能を作成する場合を除きます。


この章のGitHubリンク: BrowseZipDiff


全文検索エンジンの概要


全文検索のサポートは、リレーショナルデータベースのように標準化されていません。 ElasticsearchApache SolrWhooshXapianSphinxなど、いくつかのオープンソースフルテキストエンジンがあります。 これで十分ではないかのように! 上記のような、専用の検索エンジンに匹敵する検索機能も提供するデータベースがいくつかあります。 SQLiteMySQL、およびPostgreSQLは、テキスト検索のほか、 MongoDBCouchDBなどのNoSQLデータベースをサポートしています。


それらのどれがFlaskアプリケーションで動作できるかに興味がある場合、答えはそれらのすべてです! これはFlaskの強みの1つです。彼は仕事をしており、頑固ではありません。 それで、最良の選択は何ですか?


私の意見では、特殊な検索エンジンのリストから、Elasticsearchは特に際立っています。 そして最も人気があり重要なものとして、LogstashとKibanaとともに、ログインデックス作成用のELKスタックの「E」シンボルとして最初に登場します。 リレーショナルデータベースの1つで検索機能を使用するのは良い選択かもしれませんが、SQLAlchemyがこの機能をサポートしていないという事実を考えると、生のSQLステートメントを使用して検索を処理するか、テキストクエリにアクセスできるパッケージを見つける必要があります。 SQLAlchemyとの共有。


上記に基づいて、Elasticsearchを選択しますが、テキストのインデックス付けと検索のすべての機能を実装して、別のエンジンに簡単に切り替えられるようにします。 これにより、1つのモジュール内のいくつかの関数を書き換えるだけで、実装が別のメカニズムに基づく代替バージョンに置き換えられます。


Elasticsearchをインストールする方法はいくつかあります。たとえば、ワンクリックインストールや、自分でインストールする必要のあるバイナリを含むzipファイル、さらにはDockerイメージなどです。 ドキュメントには、これらのオプションすべてに関する詳細情報を含むインストールページがあります。 Linuxを使用している場合、おそらくディストリビューション用の手頃な価格のパッケージがあるでしょう。 Macを使用していてHomebrewがインストールされている場合は、単にbrew install elasticsearch実行できます。


コンピューターにElasticsearchをインストールした後、ブラウザーのアドレスバーにhttp://localhost:9200と入力すると、サービスに関する基本的な情報がJSON形式で返されます。


ElasticsearchはPythonから管理されるため、Pythonクライアントライブラリを使用します。


 (venv) $ pip install elasticsearch 

これで、 requirements.txtファイルを更新しても問題ありません。


 (venv) $ pip freeze > requirements.txt 

Elasticsearchチュートリアル


最初に、PythonシェルからElasticsearchを操作する基本を紹介します。 これは、このサービスに精通し、その実装を理解するのに役立ちます。これについては後で説明します。


Elasticsearchへの接続を作成するには、接続URLを引数として渡すことでElasticsearchクラスのインスタンスを作成します。


 >>> from elasticsearch import Elasticsearch >>> es = Elasticsearch('http://localhost:9200') 

Elasticsearchデータには、書き込み時にインデックスが作成されます。 リレーショナルデータベースとは異なり、これは単なるJSONオブジェクトです。 次の例では、インデックスmy_index下にあるtextタイプのフィールドにオブジェクトを書き込みます。


 >>> es.index(index='my_index', doc_type='my_index', id=1, body={'text': 'this is a test'}) 

必要に応じて、インデックスはさまざまなタイプのドキュメントを格納できます。その場合、 doc_type引数はこれらのさまざまな形式に従ってさまざまな値に設定できます。 すべてのドキュメントを同じ形式で保存するため、ドキュメントタイプをインデックス名に設定します。


保存されたドキュメントごとに、Elasticsearchは一意の識別子とデータを含むJSONオブジェクトを受け取ります。


同じインデックスで2番目のドキュメントを保存しましょう:


 >>> es.index(index='my_index', doc_type='my_index', id=2, body={'text': 'a second test'}) 

そして、このインデックスに2つのドキュメントがあるので、自由形式の検索を実行できます。 この例では、 this testを探します:


 >>> es.search(index='my_index', doc_type='my_index', ... body={'query': {'match': {'text': 'this test'}}}) 

es.search()答えは、検索結果を含むPython辞書です。


 { 'took': 1, 'timed_out': False, '_shards': {'total': 5, 'successful': 5, 'skipped': 0, 'failed': 0}, 'hits': { 'total': 2, 'max_score': 0.5753642, 'hits': [ { '_index': 'my_index', '_type': 'my_index', '_id': '1', '_score': 0.5753642, '_source': {'text': 'this is a test'} }, { '_index': 'my_index', '_type': 'my_index', '_id': '2', '_score': 0.25316024, '_source': {'text': 'a second test'} } ] } } 

ここでは、検索で2つのドキュメントが返され、それぞれに独自の評価が付けられていることがわかります。 より高い評価のドキュメントには、私が探していた2つの単語が含まれ、他のドキュメントには1つだけが含まれています。 しかし、ご覧のとおり、単語はテキストと完全に一致しないため、最良の結果であっても大きなスコアはありません。


次に、 secondという単語の結果を見てみましょう。


 >>> es.search(index='my_index', doc_type='my_index', ... body={'query': {'match': {'text': 'second'}}}) { 'took': 1, 'timed_out': False, '_shards': {'total': 5, 'successful': 5, 'skipped': 0, 'failed': 0}, 'hits': { 'total': 1, 'max_score': 0.25316024, 'hits': [ { '_index': 'my_index', '_type': 'my_index', '_id': '2', '_score': 0.25316024, '_source': {'text': 'a second test'} } ] } } 

私の検索はこのドキュメントのテキストと一致しないため、結果のスコアはかなり低くなりますが、2つのドキュメントのうち1つだけが「second」という単語を含むため、他のドキュメントはまったく表示されません。


Elasticsearchリクエストオブジェクトには多くのパラメータがあり、それらはすべて十分に文書化されています 。 それらの中には、リレーショナルデータベースの場合と同様に、ページネーションとソートがあります。


このインデックスにエントリを追加して、さまざまな検索オプションを試してみてください。 実験が終了したら、次のコマンドでインデックスを削除できます。


 >>> es.indices.delete('my_index') 

Elasticsearchの構成


Elasticsearchをアプリに統合することは、Flaskのクールさの好例です。 このサービスとPythonパッケージは、Flaskとは何の関係もありませんが、まだ十分なレベルの統合を目指しています。 設定から始めましょうapp.config辞書に記述します。


config.py:Elasticsearch構成。

 class Config(object): # ... ELASTICSEARCH_URL = os.environ.get('ELASTICSEARCH_URL') 

他の多くの設定エントリと同様に、Elasticsearchの接続URLは環境変数から取得されます。 変数が定義されていない場合、値Noneを取得します。これはElasticsearchを無効にするためのシグナルになります。 これは主に利便性のために行われます。そのため、アプリケーションで作業するとき、特に単体テストを実行するときにElasticsearchサービスが常にあるとは限りません。 したがって、サービスが使用されていることを確認するには、 ELASTICSEARCH_URL環境ELASTICSEARCH_URLを、端末で直接定義するか、次のように.envファイルを追加して定義する必要があります。


 ELASTICSEARCH_URL=http://localhost:9200 

ElasticsearchはFlask拡張機能で装飾されていないため、問題が発生します。 上記の例のように、Elasticsearchのインスタンスをグローバルスコープで作成することはできません。初期化するには、 create_app()関数を呼び出した後にのみ使用可能になるcreate_app()アクセスする必要があるためです。 そこで、アプリケーションファクトリー関数のappインスタンスにelasticsearch属性を追加することにしました。


app / init .py :Elasticsearchインスタンス。

 # ... from elasticsearch import Elasticsearch # ... def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) # ... app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URL']]) \ if app.config['ELASTICSEARCH_URL'] else None # ... 

appインスタンスに新しい属性を追加するのは少し奇妙に思えるかもしれませんが、Pythonオブジェクトの構造はあまり厳しくないため、いつでも新しい属性を追加できます。 検討できる代替手段は、その__init__()関数で定義されたelasticsearch属性を使用してFlaskをサブクラス化することです(おそらくMicroblogと呼びましょう__init__()


ElasticsearchサービスURLが環境で定義されていない場合に、 条件式を使用NoneてElasticsearchインスタンスにNoneを割り当てる方法に注目してください。



この章の概要で述べたように、Elasticsearchから他の検索エンジンへの移行の可能性を単純化したいと思います。 また、ブログ投稿を見つけるためにこの関数を特別にコーディングしたくはありませんが、将来必要に応じて他のモデルに簡単に拡張できるソリューションを開発することを好みます。 これらすべての理由から、検索機能の抽象化を作成することにしました。 アイデアは一般的な用語で関数を開発することであるため、インデックスを作成する必要があるのはPostモデルだけであるとは仮定せず、Elasticsearchが優先インデックスエンジンであるとは仮定しません。 しかし、何かについて何も推測できない場合、どうすればこの作業を行うことができますか?


最初に必要なことは、どのモデルとその中の1つまたは複数のフィールドにインデックスを付けるかを示す一般的な方法を何らかの方法で見つけることです。 インデックス付けが必要なモデルは、インデックスに含めるフィールドを含む__searchable__クラスの属性を定義する必要があると言いたいです。 Postモデルの場合、次のようになります。


app / models.py:Postモデルに__searchable__属性を追加します。

 class Post(db.Model): __searchable__ = ['body'] # ... 

このモデルには、 bodyフィールドにインデックスが必要であると書かれています。 明確にするために説明します! 追加したこの__searchable__属性は単なる変数であり、それに関連する動作はありません。 一般的な方法でインデックス作成関数を作成するのに役立ちます。


app / search.pyモジュールのElasticsearchインデックスと相互作用するすべてのコードを記述します。 このアイデアは、このモジュールにすべてのElasticsearchコードを保存することです。 アプリケーションの残りの部分は、この新しいモジュールの関数を使用してインデックスにアクセスし、Elasticsearchに直接アクセスしません。 これは重要です。ある日、Elasticsearchが気に入らず、別のエンジンに切り替えると決定した場合、このモジュールの機能を上書きするだけで、アプリケーションは引き続き動作するためです。


このアプリケーションでは、テキストのインデックス付けに関連する3つのヘルパー関数が必要であると判断しました。



Pythonコンソールから上で示した機能を使用して、Elasticsearchのこれら3つの機能を実装するapp / search.pyモジュールを次に示します。


app / search.py​​:検索機能。

 from flask import current_app def add_to_index(index, model): if not current_app.elasticsearch: return payload = {} for field in model.__searchable__: payload[field] = getattr(model, field) current_app.elasticsearch.index(index=index, doc_type=index, id=model.id, body=payload) def remove_from_index(index, model): if not current_app.elasticsearch: return current_app.elasticsearch.delete(index=index, doc_type=index, id=model.id) def query_index(index, query, page, per_page): if not current_app.elasticsearch: return [], 0 search = current_app.elasticsearch.search( index=index, doc_type=index, body={'query': {'multi_match': {'query': query, 'fields': ['*']}}, 'from': (page - 1) * per_page, 'size': per_page}) ids = [int(hit['_id']) for hit in search['hits']['hits']] return ids, search['hits']['total'] 

これらの機能はすべて、 app.elasticsearchapp.elasticsearchれているかどうかを確認することから始まります。 Noneの場合、アクションNone関数None終了します。 つまり、Elasticsearchサーバーが構成されていない場合、アプリケーションは検索機能やエラーなしで引き続き動作します。 開発時または単体テストの実行時に、これがどれほど便利かが明らかになります。


関数は名前のインデックスを引数として取ります。 Elasticsearchに渡すすべての呼び出しで、Pythonコンソールの例で既に行ったように、この名前をインデックス名として使用し、ドキュメントタイプとしても使用します。


インデックスからレコードを追加または削除する関数は、2番目の引数としてSQLAlchemyモデルを取ります。 add_to_index()関数は、モデルに追加された__searchable__クラス変数を使用して、インデックスに挿入されたドキュメントを構築します。 思い出すと、Elasticsearchドキュメントには一意の識別子も必要でした。 これには、SQLAlchemyモデルのidフィールドを使用しますが、これも一意です。 SQLAlchemyとElasticsearchに同じid値を使用すると、2つのデータベースのレコードをリンクできるため、検索を行うときに非常に便利です。 もう1つのトリックは、既存の識別子を持つレコードを追加しようとすると、Elasticsearchが古いレコードを新しいものに置き換えるため、 add_to_index()を新しいオブジェクトと変更されたオブジェクトの両方に使用できることです。


以前にremove_from_index()使用したes.delete ()関数をes.delete ()しませんでした。 この関数は、この識別子の下に保存されているドキュメントを削除します。 同じ識別子を使用して両方のデータベースのレコードをリンクする便利さの良い例を次に示します。


query_index()関数は、検索するインデックス名とテキスト、およびページネーションコントロールを受け入れ、検索結果をFlask-SQLAlchemyの結果としてページネーションできるようにします。 Pythonコンソールからes.search()関数を使用する例を見てきました。 私が発行している呼び出しはかなり似ていますが、 matchリクエストタイプを使用するmulti_match 、複数のフィールドで検索できるmulti_matchを使用することにしました。 フィールド名*により、Elasticsearchはインデックス検索のすべてのフィールドを表示できます。 異なるモデルはインデックス内で異なるフィールド名を持っている可能性があるため、これはこの関数をユニバーサルにするのに役立ちます。


es.search()body引数は、 es.search()引数でクエリ自体を補完します。 from引数とsize引数は、結果セット全体のどのサブセットを返すかを決定します。 Elasticsearchは、Flask-SQLAlchemyオブジェクトに似た本格的なPaginationネーションオブジェクトを提供しないため、 from値を計算するためにページネーションプロシージャを自分で作成する必要があります。


query_index()関数のreturnはもう少し複雑です。 2つの値を返します。1つ目は検索結果の識別要素のリスト、2つ目は結果の合計数です。 どちらもes.search()によって返されるPython辞書から派生しています。 識別子のリストを取得するために使用する式に慣れていない場合は、説明します。彼女の名前はlist comprehension (リストジェネレーター)です。これは、リストをある形式から別の形式に変換できる素晴らしいPython関数です。 この場合、リストジェネレーターを使用して、Elasticsearchが提供するはるかに大きな結果リストからid値を抽出します。


わかりにくいですか? おそらく、Pythonコンソールでこれらの機能をデモンストレーションすることで、それらをよりよく理解するのに役立つかもしれません。 次のセッションでは、データベースのすべてのメッセージをElasticsearchインデックスに手動で追加します。 私のテストデータベースには、「1」、「2」、「3」、「4」、「5」という数字を含むメッセージがいくつかあったため、検索クエリで使用しました。 データベースの内容と一致するようにクエリを調整する必要がある場合があります。


 >>> from app.search import add_to_index, remove_from_index, query_index >>> for post in Post.query.all(): ... add_to_index('posts', post) >>> query_index('posts', 'one two three four five', 1, 100) ([15, 13, 12, 4, 11, 8, 14], 7) >>> query_index('posts', 'one two three four five', 1, 3) ([15, 13, 12], 7) >>> query_index('posts', 'one two three four five', 2, 3) ([4, 11, 8], 7) >>> query_index('posts', 'one two three four five', 3, 3) ([14], 7) 

送信したクエリは7つの結果を返します。 最初は、1ページあたり100ポイントで1ページを要求し、7つすべてを取得しました。 その後、次の3つの例は、結果がSQLAlchemyオブジェクトではなく識別子のリストの形式になることを除いて、Flask-SQLAlchemyで行った方法と非常によく似た方法でページをページングできる方法を示しています。


すべてをきれいに保ちたい場合は、実験後にpostsインデックスを削除します。


 >>> app.elasticsearch.indices.delete('posts') 

SQLAlchemyとの検索統合


前のセクションで説明した解決策は受け入れられますが、まだ解決されていない問題がいくつかあります。 最初の、そして最も明白なものは、結果が数値識別子のリストの形式になるということです。 これは非常に不便です。なぜなら、視覚化のためにテンプレートに渡すことができるようにSQLAlchemyモデルが必要であり、この数値のリストをデータベースの適切なモデルに置き換える方法が必要だからです。 2番目の問題は、このソリューションでは、アプリケーションが明示的にインデックス呼び出しを発行する必要があることです。 SQLAlchemy側で変更を行うときに不在着信インデックスの原因となるエラーは簡単には検出されないため、メッセージは追加または削除されます。 2つのデータベースは、エラーが発生するたびに同期がとれなくなり、おそらくしばらくは気付かないでしょう。 より良い解決策は、SQLAlchemyデータベースを変更するときにこれらの呼び出しを自動的に有効にすることです。


識別子をオブジェクトに置き換える問題は、データベースからこれらのオブジェクトを読み取るSQLAlchemyクエリを作成することで解決できます。 簡単に聞こえますが、実際には単一のリクエストの効率的な実行を実装するのはそれほど簡単ではありません。


2番目の問題(インデックス作成の変更の自動追跡)を解決するために、SQLAlchemyイベントからElasticsearchインデックスを更新することにしました。 SQLAlchemyは、アプリケーションが通知を受け取ることができるイベントの大きなリストを提供します。 たとえば、セッションのコミット( 変更のコミット )ごとに、SQLAlchemyセッションで行われたのと同じ更新をElasticsearchインデックスに適用できる、SQLAlchemyによって呼び出されるアプリケーションの関数を使用できます。


これら2つのタスクのソリューションを実装するために、 ミックスインクラスを作成します。 ミックスインクラスを覚えていますか? 第5章では 、Flask-LoginのUserMixinクラスをユーザーモデルに追加して、Flask-Loginに必要な機能の一部を委任しました。 検索をサポートするために、モデルに接続すると、SQLAlchemyモデルに関連付けられたフルテキストインデックスを自動的に制御できるようにする独自のSearchableMixinクラスを定義します。 mixinクラスは、SQLAlchemyとElasticsearchの世界を「接続する」レイヤーとして機能し、上記の2つの問題の解決策を提供します。


実装を示してから、興味深い詳細を見ていきます。 いくつかのベストプラクティスの使用に注意してください。したがって、このコードを十分に理解するには、このコードを慎重に検討する必要があります。


app / models.py:SearchableMixinクラス。

 from app.search import add_to_index, remove_from_index, query_index class SearchableMixin(object): @classmethod def search(cls, expression, page, per_page): ids, total = query_index(cls.__tablename__, expression, page, per_page) if total == 0: return cls.query.filter_by(id=0), 0 when = [] for i in range(len(ids)): when.append((ids[i], i)) return cls.query.filter(cls.id.in_(ids)).order_by( db.case(when, value=cls.id)), total @classmethod def before_commit(cls, session): session._changes = { 'add': list(session.new), 'update': list(session.dirty), 'delete': list(session.deleted) } @classmethod def after_commit(cls, session): for obj in session._changes['add']: if isinstance(obj, SearchableMixin): add_to_index(obj.__tablename__, obj) for obj in session._changes['update']: if isinstance(obj, SearchableMixin): add_to_index(obj.__tablename__, obj) for obj in session._changes['delete']: if isinstance(obj, SearchableMixin): remove_from_index(obj.__tablename__, obj) session._changes = None @classmethod def reindex(cls): for obj in cls.query: add_to_index(cls.__tablename__, obj) db.event.listen(db.session, 'before_commit', SearchableMixin.before_commit) db.event.listen(db.session, 'after_commit', SearchableMixin.after_commit) 

このmixinクラスには4つの関数があります—クラスのすべてのメソッド( classmethodデコレータを参照)。 これは、特定のインスタンスではなく、クラスに関連付けられている特別なメソッドです。 このメソッドがインスタンスではなくクラスを最初の引数として受け取ることが明確になるように、通常のインスタンスメソッドで使用されるself引数の名前をclsたことに注意してください。 たとえば、 Postモデルに接続する場合、上記のsearch()メソッドは、Postクラスの実際のインスタンスを持たずにPost.search()として呼び出されます。


search()クラスメソッドは、 app / search.pyquery_index()関数をラップして、オブジェクト識別子のリストを実際のオブジェクトに置き換えます。 明らかに、この関数が最初に行うことはquery_index()呼び出し、 cls.__ tablename__をインデックスの名前として渡すことです。 この規則により、すべてのインデックスには、リレーショナルテーブルに割り当てられたFlask-SQLAlchemyという名前が付けられます。 この関数は、結果IDとその総数のリストを返します。 IDでオブジェクトのリストを取得するSQLAlchemyクエリは、データベースからの結果が識別子と同じ順序になるようにするために使用するSQL言語のCASEステートメントに基づいています。 Elasticsearchクエリは、関連性の高いものから低いものにソートされた結果を返すため、これは重要です。 このクエリの仕組みについて詳しく知りたい場合は、このStackOverflowの質問への回答を参照できます。 search() , , .


before_commit() after_commit() SQLAlchemy, . before , , , , , session.new session.dirty session.deleted . , . session._changes , , , Elasticsearch.


after_commit() , , Elasticsearch. _changes , before_commit() , , app/search.py .


reindex() — , . , - Python , . , Post.reindex(), ( search index ).


, db.event.listen() , . , before after . Post .


SearchableMixin Post , , befor after :


app/models.py : SearchableMixin class Post model.

 class Post(SearchableMixin, db.Model): # ... 

reindex() , :


 >>> Post.reindex() 

, SQLAlchemy, Post.search() . :


 >>> query, total = Post.search('one two three four five', 1, 5) >>> total 7 >>> query.all() [<Post five>, <Post two>, <Post one>, <Post one more>, <Post one>] 


, . . .


- , , q URL-. , Python Google, , URL- , :


 https://www.google.com/search?q=python 

URL- , , .


, - . POST , , , , GET , , URL- . , , .


, q :


app/main/forms.py : Search form.

 from flask import request class SearchForm(FlaskForm): q = StringField(_l('Search'), validators=[DataRequired()]) def __init__(self, *args, **kwargs): if 'formdata' not in kwargs: kwargs['formdata'] = request.args if 'csrf_enabled' not in kwargs: kwargs['csrf_enabled'] = False super(SearchForm, self).__init__(*args, **kwargs) 

q , , . . , , , Enter , . __init__ , formdata csrf_enabled , . formdata , Flask-WTF . request.form , Flask , POST . , GET , , Flask-WTF request.args , Flask . , , CSRF- , CSRF, form.hidden_tag() . CSRF- , csrf_enabled False , Flask-WTF , CSRF .


, , SearchForm , . , , . , ( route ), ( templates ), , , . before_request , 6 , . , , , :


app/main/routes.py : before_request handler.

 from flask import g from app.main.forms import SearchForm @bp.before_app_request def before_request(): if current_user.is_authenticated: current_user.last_seen = datetime.utcnow() db.session.commit() g.search_form = SearchForm() g.locale = str(get_locale()) 

, . , , , , , - . - g , Flask. g , Flask, , , . g.search_form , , before Flask , URL, g , , . , g , , - , g , , , .


— . , , . , , g , render_template() . :


app/templates/base.html : .

  ... <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> ... home and explore links ... </ul> {% if g.search_form %} <form class="navbar-form navbar-left" method="get" action="{{ url_for('main.search') }}"> <div class="form-group"> {{ g.search_form.q(size=20, class='form-control', placeholder=g.search_form.q.label.text) }} </div> </form> {% endif %} ... 

, g.search_form . , , , . , . method get , , GET . , action empty, , . , , , , , .



- , . view — /search , http://localhost:5000/search?q=search-words , Google.


app/main/routes.py : Search view function.

 @bp.route('/search') @login_required def search(): if not g.search_form.validate(): return redirect(url_for('main.explore')) page = request.args.get('page', 1, type=int) posts, total = Post.search(g.search_form.q.data, page, current_app.config['POSTS_PER_PAGE']) next_url = url_for('main.search', q=g.search_form.q.data, page=page + 1) \ if total > page * current_app.config['POSTS_PER_PAGE'] else None prev_url = url_for('main.search', q=g.search_form.q.data, page=page - 1) \ if page > 1 else None return render_template('search.html', title=_('Search'), posts=posts, next_url=next_url, prev_url=prev_url) 

, form.validate_on_submit() , , . , , POST , form.validate() , , , . , , , , .


Post.search() SearchableMixin . , Pagination Flask-SQLAlchemy. , Post.search() .


, , , - . index.html , , , search.html , , _post.html sub- :


app/templates/search.html : .

 {% extends "base.html" %} {% block app_content %} <h1>{{ _('Search Results') }}</h1> {% for post in posts %} {% include '_post.html' %} {% endfor %} <nav aria-label="..."> <ul class="pager"> <li class="previous{% if not prev_url %} disabled{% endif %}"> <a href="{{ prev_url or '#' }}"> <span aria-hidden="true">&larr;</span> {{ _('Previous results') }} </a> </li> <li class="next{% if not next_url %} disabled{% endif %}"> <a href="{{ next_url or '#' }}"> {{ _('Next results') }} <span aria-hidden="true">&rarr;</span> </a> </li> </ul> </nav> {% endblock %} 

. Bootstrap .



? , . . , Elasticsearch, , , app/search.py . , , , , SearchableMixin , __searchable__ SQLAlchemy. , , .


ここに 戻る



Source: https://habr.com/ru/post/J351900/


All Articles