A Day In The Boy's Life -40ページ目

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

華やかに始まったプロジェクトも時の流れとともに衰退し、今となってはあの時の栄光と熱意はどこに?といったチームの惨状を見ることはしばしばあります。

ユーザーに提供するサービスの場合は、そのユーザー離れにより衰退していく様を利用する立場としてよく目にしますが、社内情報システム部門のそれは構築・運用チームは存在しており、その会社のシステム基盤として維持・管理はしていかないといけないんだけど、チームとしての士気は下がりきっていてどうしようもない状況というのはより悲惨な光景かもしれません。

何でそんな状況にいたったんでしょうか?



1. 外部リソースの総動員


プロジェクトの始まりは極めて華やかだったりします。

予算が確保され、タスクフォース的にその道のプロが要員計画に組みこれます。

そこには下請けやコンサルタントなども含めて大量の外部リソースが流入してきます。


プロジェクト運営において、外部リソースを使うのはスケジュールであったり、そもそも自社にそのスキルやノウハウが無いという状況であれば外部から取り入れるというのは至極当たり前のことではあるのですが、現場のエンジニアから見たらあまり外部のリソースに頼る状況うれしいものではありません。

自分たちだけではできないのか、という信頼されていないような疑念を持ったりしますし、自分たちの畑を荒らされるのが嫌だという気分にもなってきます


自社の文化と合わない人も来たりして、プロジェクト期間中は職場の雰囲気がだいぶ変わることもあり、ホームなのにアウェイ感が出て仕事のやりにくさを感じ出すエンジニアもいます。

特に、プロジェクトによって招き入れた人がプロジェクトマネージャやオーナーのつてだったりした場合は最悪で、職場内での立場が逆転することさえあります。

こうなってくるとますますチームや組織への不満や信頼がなくなってくるわけです。



2. 残らないスキル


プロジェクトは当然紆余曲折ありながらもゴールへと着地します。

しかし、社内情報システムの場合はここがスタートラインです。

長い長い運用フェーズの始まりです。

リリース後の1年近くはシステムが安定しなかったり、プロジェクトの制約により延期された機能の実装、ユーザー部門からのフィードバックを受けてブラッシュアップの期間が続きます。

そして、システムは安定期に入ります。


こうなってくるとプロジェクト開始当初にいた大量のリソースは当然不要になりますので、1人また1人とチームを去っていきます。

残るのはアップデートされていない大量のドキュメントと口伝によるノウハウです。

そして、その情報はますます陳腐化していきもはや誰も真実の仕様をしらないか、一部の生き残りが生きる伝説として重宝されだしたりします。


プロジェクト期間中のコアメンバーが外部リソースで塗り固められていた場合、社内にはそのスキルを持っている人がいないので、もはや末路は火を見るより明らかで触らぬ神になんとやらと言うように触れないシステムが不良資産として残るわけです。



3. 持てない愛着と誇り


不良資産を抱えたチームは当然それを保守・運用するモチベーションを見出しにくくなります。

自分が作り出したものであれば何時までも愛着によって維持できるかもしれませんが、他人が作ったものとなるとなお更かもしれません。


なんせ自分でもよくわからないシステムなわけです。

現状維持することに精一杯になっていますし、これは自分が作ったものだ!自分がこのサービスを動かしているのだ!なんて誇りを持てないわけですから、それをよりよい方向にしようなんて考えを誰も持たなくなってきます

取りあえず動いているサービスを維持することが仕事となり、そのサービスがどう思われようと関係ない気分になってきます。



4. 存在しない危険因子


社内システムの場合、環境はなるべく安定するように意思決定されますので、ライフサイクルが長くなりがちですし、危険因子が少ないのでリスクをとるようなことを避けたりします

なるべく動いている今の環境を活かそうとしますし、外部サービスのように別のサービスによって客を持っていかれたり食いつぶされるというリスクがありません。

これによってシステムは安定するかもしれませんが、誰もそこに作る楽しさや運営する喜びを見出すのが難しくなってきます。


「システム部を変えよう!」なんてスローガンが常套句のように毎年発表されますが、危険因子がない分、自分たちにさえ変え方がわからなかったりします。

当然社内からの圧力や不満というのは肌で感じることがあるので、変わらなきゃいけないという焦りを持つことはあるのですが、変わることでのメリットより変えることでのリスクを嫌います。


安定した環境でのエンジニアは新たに得るものが無いのでスキルを磨いたり、多少火を噴いたりしつつも収束に向かって結束して乗り切ったときの達成感を得ることがないわけです。

変化が無いところにモチベーションは見出させません



5. 主導権を握れない


モチベーションがあがらない仕事に対して自ら先導するような行動を取ることは難しいでしょう。

あえて行動するとすれば、自分たちに降りかかるリスクをなるべく回避しようとするマイナスのモチベーションです。

「いや、この時期は忙しいから無理だ」と自分たちを守るために機能改修の依頼を断り、「業務が回らなくなるからこういった機能を付けてくれないと困る」という部分最適化した依頼がユーザー部門から来たりしてその応戦だけに多くの時間を費やしているシーンをよく見かけます。


ユーザー部門もシステムのことはわからず、自分たちの業務をよりよくするためという名目であれやこれや注文を伝えてくるわけですが、当然業務的には正しくともシステム運営的に正しいとは限りません

システム部門が面倒が無いように行動をすることで、やがて当初の綺麗に設計されたコードは崩れ落ち、受身のシステム部門は何時までも主導権を握れずにいます。

言われるままに追加したいくつもの使わないボタンが画面を埋め尽くし、めったに使わない機能に多くの工数が費やされそして消えていきます。


自ら主導権を握れず握ろうともしないので、システム部としての存在意義を発揮できないでいるわけです。



まとめ


見てきた中での一例として書いていきましたが、後手後手に回る計画がますます身動きを取れない組織を作り出していきますし、今を乗り切るための安易な施策が未来の組織をどんどん壊していきます。


やはりシステム部というからにはその中心にはエンジニアがいないと成り立たないと思いますし、そのエンジニアのモチベーションを上げるためには、自分たちがその業務やサービスをコントロールしているのだという、中の人としての意識を芽生えさせてあげるということにあるのではないかと感じています。




若いエンジニアに仕事を頼むと、時にやりすぎなところがあったりして悩むときがあったりします。

それはあらぬ方向に突っ走られるというよりは、頼んだ以上のことを実装したりするといったシーンです。

それって別にいいことなんじゃないの?って思われるときもあるのですが、プロジェクトを管理しているとそこに時間を割くのではなく他に手を回したい、でも作ったものはそれはそれで素敵な機能なんで叱るに叱れないといった状況に悩んだりするわけです。



うまく暴走させてあげるのに大事なレール


特に経験の浅いエンジニアは作る楽しさを覚えて間もなかったりもしますから、予想以上に工数かかっているなって確認してみると想像で突っ走ったりしてとんでもないものを作ろうとしていることはしばしばあります。

ちゃんと仕様を伝えていないこちらも悪いところがあるのですが、あまり細かく管理していくのもエンジニア自身の成長の妨げになりますし、自分もそうやって自由に作ることでスキルを磨いてこれたりしたものですから、自分の言うものだけを作れというのはあまりよくないなと考えていたりします。


視点が狭いというかどうしても技術よりな観点になってしまうのもうなずけるのですが、何を作るのかというよりはどう実装するかという観点に考えがよりがちです。

ドアを作ってくれと頼むと自動ドアを作ろうとしていたりするのですが、こっちとしてはそもそも人を通すための出入り口があればよいわけで最悪不恰好な穴でもあけてくれればいいわけですが、職人気質が働くのか立派なドアを一生懸命に作ろうとするわけです。

何でここにドアを作らないといけないのかという視点ではなく、とにかくドアを作らないといけない、どうやってどんなドアを作ろうかということに偏向してしまっているのをよく見かけたりします。

こういったことは自分自身にも当然経験があるわけで、視点を広げるというのはある程度の経験を経てじゃないとなかなか難しいことではありますから、上がきちんと間違った方向に進んでいないかコントロールしてあげる必要はあるなと感じています。


もう一つありがちなのが、与えた仕事から飛びついてしまうという点です。

今、しかかり中のものがあるにもかかわらず「Aくん、この件ちょっと相談したいんだけどクライアントからの要望でこういう機能を作ってくれない?」というと、言われたことだからすぐにやらないといけないというバイアスが働くのか、優先度の付け方がうまくできずにLIFO形式にタスクをこなしてしまったりします。

ですので、こういった事にならないようにあえて情報を渡さないというような管理も必要になってきたりします。

そんなに優先度の高くない仕事であれば、たとえ内容がまとまっていても本人に伝えずに温存しておくというか。

優先度の管理というものを引き取ってしまった方が本人的にも目の前のことに集中できますし、こちらとしても複数のタスクを同時並行的に見る必要も無いのでスケジュールの進捗が見えやすかったりします。


ただ、先に書いたようにこと細かい管理というのはエンジニア自身のモチベーションを下げることにもなりますし、こちらもそんな細かく管理するだけの余力もありません。

きちんとゴール設定をしてあげ、マイルストンごとに成果物を確認していくようにしてベクトルが違う方向にむき出したら都度修正していくというような形で踏み外さないレールを敷いておくというのは必要かなと思うわけです。



暴走の果ての成果を無駄にしない


エンジニアの情熱というのは、そのシステムを組み上げる原動力の1つです。

ですから、例え突っ走ってしまって自分の想像の斜め上をいくものができたとしても頭ごなしに否定して叱るというのはよくないと思います。

むしろ、その成果物をどう使うかという方が本人のモチベーションを下げないですし、自動ドアができたらできたで使い道は全然あるわけです。


もし、通常は使わないような機能が実装されてしまったとしても、オプションとして扱ってしまえば便利機能の一つということで片付けられたりもしますから成果を無駄にせずにうまく活かす方法を探した方が賢明です。

そういった成果は独りよがりで作ってしまったものだったりもしますので、機能の目的として論点がずれてたりするものが多かったりもするのですが、要らないと捨てるよりもいっそのこと、その機能をより拡張していく方向に進めてしまえば結果的に正しい方向に突っ走ったことにしてしまうこともできます。


こういったエンジニアの暴走を目の当たりにしてしまうと「何でそんなことに時間を割いているんだ」という気分にもなってしまうこともあるのですが、よくよく考えてみると管理する立場の人がその人の能力であったり、プロジェクトのスケジュールやタスクの優先度を鑑みて設定した仕様であり、その仕様を超えるような成果であれば怒る理由もありません。

意外とそこまでの機能は要らないと思っているのは自分だけであったりして、クライアントに見せると喜ばれたりもします。

まぁ、元々単なるドアを作る想定で工数を積んでいたものが、その期間で自動ドアができたんなら万々歳って気分になるのはわかるんですが、管理する立場としてその暴走を見て気が気じゃないのは単に自分の決めた枠にはまらず行動しているというエゴなのかもしれません。


コミュニケーションミスで全く逆方向のものを作り上げられたらそれは大問題だったりもしますが、とりあえず前に進んでいるのであれば、こういったエンジニアの情熱をうまく活かして正しい暴走をさせてあげるのが重要なんだなと思います。

暴走するほどの情熱を削ぐのは管理者として正しくないでしょうし、そのパワーって他に変えがたく与えようとしてもなかなか与えられないわけですから、右往左往しながらも前へ前へ推し進めていくマネジメントが必要なんだなと感じています。




前回の「Apache Solrのデモ環境を作ってみる 」にて動く環境が作れたので、今回はもう少し突っ込んだApache Solrの設定ファイル周りについて書いていきたいと思います。



Apache Solrの設定ファイルの中身


Apache Solrの環境設定で触るファイルは大きく2つあります。

1つは、Solrの動作自体を定義するsolrconfig.xmlファイル。

そしてもう1つは、Solrに取り込むデータのスキーマ情報を定義するschema.xmlファイルです。

何れも、


/path/to/solr/example/solr/collection1/conf

に、存在します。

先のエントリでも書きましたがcollection1はSolrのコアディレクトリとなるため、コアを追加したらそのコアごとに設定ファイルが存在します。


順に説明したいところではありますが、solrconfig.xmlファイル(Solrの管理メニューのConfigメニューで見える内容もこのファイル)はSolrの動作を定義したもので、Solrが利用するjarファイルのパスやディレクトリの定義をしたり、検索クエリの細かな仕様やキャッシュ周りの設定などができるのですが、そのままでも取あえずは動くため一旦割愛します。


今回は、前回のデモ環境で作った住所録データベースのデータインポート作業をしたときに編集した、schema.xmlファイルについて少しだけ触れてみます。

下記が、前回のデモ環境用の作ったschema.xmlの全体です(一部コメントなどを割愛しています)。


<?xml version="1.0" encoding="UTF-8" ?>
<schema name="example" version="1.1">
    <types>
        <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
        <fieldType name="date" class="solr.DateField" sortMissingLast="true" omitNorms="true" />
        <fieldType name="long" class="solr.LongField" omitNorms="true" />
        <fieldType name="textTight" class="solr.TextField" positionIncrementGap="100" >
            <analyzer>
                <tokenizer class="solr.WhitespaceTokenizerFactory" />
                <filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="false" />
                <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
                <filter class="solr.WordDelimiterFilterFactory" generateWordParts="0" generateNumberParts="0" catenateWords="1" catenateNumbers="1" catenateAll="0" />
                <filter class="solr.LowerCaseFilterFactory" />
                <filter class="solr.RemoveDuplicatesTokenFilterFactory" />
            </analyzer>
        </fieldType>

        <!-- N-gram analyzed type using CJKAnalyzer -->
        <fieldType name="text_cjk" class="solr.TextField">
            <analyzer class="org.apache.lucene.analysis.cjk.CJKAnalyzer" />
        </fieldType>

        <!-- Morphologically analyzed type using JapaneseAnalyzer -->
        <fieldType name="text_ja" class="solr.TextField">
            <analyzer class="org.apache.lucene.analysis.ja.JapaneseAnalyzer" />
        </fieldType>
    </types>

    <fields>
        <field name="id"           type="long"      indexed="true" stored="true" required="true" />
        <field name="organization" type="textTight" indexed="true" stored="true" />
        <field name="zip-old"      type="textTight" indexed="true" stored="true" />
        <field name="zip"          type="textTight" indexed="true" stored="true" />
        <field name="prefecture"   type="text_ja"   indexed="true" stored="true" termVectors="true" termPositions="true" />
        <field name="district"     type="text_cjk"  indexed="true" stored="true" termVectors="true" termPositions="true" />
        <field name="town"         type="text_ja"   indexed="true" stored="true" termVectors="true" termPositions="true" />
        <field name="prefecture-undevided" type="string" indexed="true" stored="true" termVectors="true" termPositions="true" />
        <field name="full_address" type="text_ja" indexed="true" stored="true" termVectors="true" termPositions="true" multiValued="true" />
        <field name="timestamp" type="date" indexed="true" stored="true" default="NOW" multiValued="false" />
        <field name="_version_" type="long" indexed="true" stored="true"/>
    </fields>

    <uniqueKey>id</uniqueKey>

    <copyField source="prefecture" dest="prefecture-undevided" />
    <copyField source="prefecture" dest="full_address" />
    <copyField source="district"     dest="full_address" />
    <copyField source="town"             dest="full_address" />

    <defaultSearchField>full_address</defaultSearchField>

    <solrQueryParser defaultOperator="OR" />
</schema>


schmea.xmlは大きく3つの領域に分けて定義します。

1つ目がtypesタグで括られた利用するフィールドタイプやトークナイザーやフィルタを定義する箇所、2つ目がfieldsタグで括られた実際にSolrへ取り込む際のデータフィールドを定義する箇所、そして3つ目がその他のSolrの設定や取り込む際のデータ加工を指示する箇所です。



Solrで利用するfieldTypeを定義する


設定する1つ目の項目にあるtypesタグ内に定義するのは、フィールドタイプやトークナイザーなどを定義する箇所です。

ここでは、Solrへ取り込む際のデータの型を決めたり、取り込む際の解析に利用するフィルターや単語を解析するためのトークナイザーを定義します。

トークナイザーが何をするものなのかは、下記の記事が参考になるかと思います。


検索エンジンの常識をApache Solrで身につける (1/4) @IT


フィールドタイプは、後のfieldsタグの項目と連動します。

例えば、


<fieldType name="long" class="solr.LongField" omitNorms="true" />

という記述でlong型のフィールドを定義していますが、


<field name="id" type="long" indexed="true" stored="true" required="true" />

の中で、そのlong型を利用することを宣言しています。

要は、データをSolrに取り込んだり検索をする際にそのデータをどう取り込めば(扱えば)よいのかを決めているわけです。


トークナイザーは、データの中から意味ある単語をどう抽出するのかというものですが、形態素解析とN-gramの2種類があり、上記の設定ファイルの中では


<!-- N-gram analyzed type using CJKAnalyzer -->
<fieldType name="text_cjk" class="solr.TextField">
<analyzer class="org.apache.lucene.analysis.cjk.CJKAnalyzer" />
</fieldType>

<!-- Morphologically analyzed type using JapaneseAnalyzer -->
<fieldType name="text_ja" class="solr.TextField">
<analyzer class="org.apache.lucene.analysis.ja.JapaneseAnalyzer" />
</fieldType>

のように、「text_cjk」(N-gram用)と「text_ja」(形態素解析用)にて定義しています。

そして、実際の住所録データの中では、


<field name="district" type="text_cjk" indexed="true" stored="true" termVectors="true" termPositions="true" />
<field name="town" type="text_ja" indexed="true" stored="true" termVectors="true" termPositions="true" />

の中で使われており、このデータの中では市や区のデータが入った「district」には「text_cjk」(N-gram)が、町村などのデータが入った「town」には「text_ja」(形態素解析)が利用されています(何故使い分けているのかはわかりませんが)。


その他に、Solrで用意されている幾つかのトークナイザやフィルタを組み合わせたオリジナルのフィールドタイプを作ることもできます。


<fieldType name="textTight" class="solr.TextField" positionIncrementGap="100" >
<analyzer>
<tokenizer class="solr.WhitespaceTokenizerFactory" />
<filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="false" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
<filter class="solr.WordDelimiterFilterFactory" generateWordParts="0" generateNumberParts="0" catenateWords="1" catenateNumbers="1" catenateAll="0" />
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.RemoveDuplicatesTokenFilterFactory" />
</analyzer>
</fieldType>

上記では、トークナイザーに「WhitespaceTokenizerFactory」が利用されています。

これは、単純にスペースの区切りで単語を分解するトークナイザです。

その他に、フィルターの中で定義している「SynonymFilterFactory」は、単語の別名を定義するものであったり、「StopFilterFactory」ではNGワードを定義したりもできます。

利用できるトークナイザーやフィルタは、マニュアル にも記載しているのでそちらもあわせて確認してください。



Solrで利用するfieldsを定義する


設定する2つ目の項目のfieldsタグでは、Solrに取り込む各フィールドのデータ型や取り込む際のオプションを定義します。

データ型の話は、先ほど書いたとおりでtypesの中で定義したデータ型を各フィールドにて定義していきます。

fieldsタグの中には幾つかの属性を設定でき、データの扱いを制御できます。

今回のschema.xmlで追加されている属性とその意味は下記の通りです。


属性 設定できる値 意味
indexed true | false フィールドをインデックス(検索対象)するかどうか
stored true | false フィールドを検索結果に含めるかどうか(インデックスはするけど検索結果には含めたくないというようなことができる)
multiValued true | false 複数の検索フィールドを追加することを許可するかどうか
required true | false 必須項目にするかどうか
omitNorms true | false 検索スコアに関連する項目でそのキーワードのスコアの平均点を保持するかどうか(検索スコアに関連しない項目などの場合、falseに設定することでメモリ使用量を抑えることができるらしい)
termVectors true | false 検索キーワードが含まれる数やキーワードの開始、終了位置などを結果に含めるかどうか(termPositionsとtermOffsetsをtrueに設定した場合、termVectorsもtrue扱いとなる)
termPositions true | false 検索キーワードの含まれる位置を返すかどうか
termOffsets true | false 検索キーワードが含まれるオフセットを返すかどうか
default デフォルトに設定したい文字列 そのフィールドのデフォルト値


また、この定義ファイルの中では書かれていませんが、ダイナミックフィールドといってアスタリスクを使って、フィールド名(name)に特定の文字が含まれるものを一括して特定のデータ型や属性を指定するというやり方もできたりします。

属性に設定できる値も含めて、マニュアル を参考にしてみてください。



schema.xmlで定義するその他の設定


3つ目の設定項目では、データフィールドを加工したり検索の挙動を定義するオプションを書いたりします。
今回の設定ファイルの中で、最初に登場するのが


<uniqueKey>id</uniqueKey>

というものですが、これは見ての通りデータの中のユニークキーが何なのかを定義しています。

SolrのRESTなAPIでは、データに対して更新や削除をする際にIDを指定して行うため一意なキーが必要となります。


次に、


<copyField source="prefecture" dest="prefecture-undevided" />
<copyField source="prefecture" dest="full_address" />
<copyField source="district"     dest="full_address" />
<copyField source="town"             dest="full_address" />

という設定ですが、こちらは既に定義済みのフィールドを別のフィールドにコピーするという設定です。

上記の場合、都道府県から市区町村までのデータをコピー(source)してfull_addressという新しいフィールドを作って(dest)います。

検索は通常どこか特定のフィールドに対して行うため、このfull_addressのように複数のデータを連結したフィールドを作っておけば検索にヒットさせやすくなったりします。


続いては、


<defaultSearchField>full_address</defaultSearchField>

という設定項目は、先ほど作ったfull_addressのフィールドをデフォルトの検索フィールドに指定しています。

そして、最後に


<solrQueryParser defaultOperator="OR" />

検索オプションとして、AND検索にするのかOR検索にするのかを指定しています。


このように、schema.xmlではデータの型を定義したり取り込みデータのフィールドやその加工、そして検索のオプションなどが指定できます。

schema.xmlはデータ取り込みの際に必ず必要となりますので、自分の環境に合わせて参考にしてみてください。